From df08750f9eca7329fca5d30d08f71e597fd3ff53 Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Sun, 7 Jun 2026 10:12:53 +0530 Subject: [PATCH 1/7] Added the PWA installation and feture enhancement for ios web users --- ios/Podfile.lock | 13 + ios/Runner.xcodeproj/project.pbxproj | 18 + lib/core/utils/pwa_prompt.dart | 162 +++++++++ lib/core/utils/pwa_prompt_stub.dart | 6 + lib/core/utils/pwa_prompt_web.dart | 15 + lib/features/auth/screens/login_screen.dart | 3 + .../student/screens/scanner_screen.dart | 86 +---- .../student/widgets/qr_scanner_view.dart | 321 ++++++++++++++++++ pubspec.lock | 24 +- pubspec.yaml | 2 +- vercel.json | 15 + web/index.html | 55 ++- web/manifest.json | 13 +- 13 files changed, 602 insertions(+), 131 deletions(-) create mode 100644 lib/core/utils/pwa_prompt.dart create mode 100644 lib/core/utils/pwa_prompt_stub.dart create mode 100644 lib/core/utils/pwa_prompt_web.dart create mode 100644 lib/features/student/widgets/qr_scanner_view.dart create mode 100644 vercel.json diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cd58742..08ac6df 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,15 +1,28 @@ PODS: + - barcode_scan2 (0.0.1): + - Flutter + - SwiftProtobuf (~> 1.33) - Flutter (1.0.0) + - SwiftProtobuf (1.38.0) DEPENDENCIES: + - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - Flutter (from `Flutter`) +SPEC REPOS: + trunk: + - SwiftProtobuf + EXTERNAL SOURCES: + barcode_scan2: + :path: ".symlinks/plugins/barcode_scan2/ios" Flutter: :path: Flutter SPEC CHECKSUMS: + barcode_scan2: 4e4b850b112f4e29017833e4715f36161f987966 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + SwiftProtobuf: d724b5145bfc609d9a49c1e3e3a3dabb07273ffb PODFILE CHECKSUM: 5b09406f5533f670a98a35b5bf281f156d27c54b diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index da2ff1f..f7ba98e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -205,6 +205,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 05E86B82B9FAE641430CFEF1 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -282,6 +283,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 05E86B82B9FAE641430CFEF1 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/lib/core/utils/pwa_prompt.dart b/lib/core/utils/pwa_prompt.dart new file mode 100644 index 0000000..4227fc0 --- /dev/null +++ b/lib/core/utils/pwa_prompt.dart @@ -0,0 +1,162 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +// Conditional import: only the web variant accesses dart:js_interop. +import 'pwa_prompt_stub.dart' + if (dart.library.js_interop) 'pwa_prompt_web.dart'; + +/// Shows an "Add to Home Screen" bottom sheet after login, +/// but ONLY when running as a web app on iOS Safari (not yet installed as PWA). +/// +/// Usage: call [PwaPrompt.showIfNeeded] after navigating post-login. +class PwaPrompt { + PwaPrompt._(); + + static bool _alreadyShown = false; + + static Future showIfNeeded(BuildContext context) async { + if (!kIsWeb) return; + if (_alreadyShown) return; + if (!PwaDetector.shouldShow()) return; + + _alreadyShown = true; + await Future.delayed(const Duration(milliseconds: 700)); + if (!context.mounted) return; + + await showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (_) => const _InstallHintSheet(), + ); + } +} + +class _InstallHintSheet extends StatelessWidget { + const _InstallHintSheet(); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Container( + margin: const EdgeInsets.fromLTRB(16, 0, 16, 16), + decoration: BoxDecoration( + color: const Color(0xFF1C1C2E), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withAlpha(20)), + ), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Drag handle + Container( + width: 40, height: 4, + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: BorderRadius.circular(2), + ), + ), + // Header row + Row(children: [ + Container( + width: 48, height: 48, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF6C63FF), Color(0xFF3B82F6)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon(Icons.schedule_rounded, + color: Colors.white, size: 26), + ), + const SizedBox(width: 16), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Install Schedulify', + style: TextStyle( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.w700)), + SizedBox(height: 2), + Text('Get the full app experience on iOS', + style: TextStyle(color: Colors.white54, fontSize: 13)), + ], + ), + ), + ]), + const SizedBox(height: 20), + const _StepRow( + icon: Icons.ios_share_rounded, + text: 'Tap the Share button', + sub: 'at the bottom of your browser bar', + ), + const SizedBox(height: 12), + const _StepRow( + icon: Icons.add_box_outlined, + text: 'Tap "Add to Home Screen"', + sub: 'then tap Add in the top right', + ), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + backgroundColor: Colors.white.withAlpha(15), + ), + child: const Text('Maybe later', + style: TextStyle(color: Colors.white70, fontSize: 15)), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _StepRow extends StatelessWidget { + final IconData icon; + final String text; + final String sub; + const _StepRow( + {required this.icon, required this.text, required this.sub}); + + @override + Widget build(BuildContext context) { + return Row(children: [ + Container( + width: 40, height: 40, + decoration: BoxDecoration( + color: Colors.white.withAlpha(13), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: Colors.white70, size: 20), + ), + const SizedBox(width: 14), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(text, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600)), + Text(sub, + style: const TextStyle(color: Colors.white38, fontSize: 12)), + ], + ), + ]); + } +} diff --git a/lib/core/utils/pwa_prompt_stub.dart b/lib/core/utils/pwa_prompt_stub.dart new file mode 100644 index 0000000..3079514 --- /dev/null +++ b/lib/core/utils/pwa_prompt_stub.dart @@ -0,0 +1,6 @@ +/// Stub used on non-web platforms. +/// On native (Android/iOS) this class always returns false, +/// so the PWA prompt is never shown. +class PwaDetector { + static bool shouldShow() => false; +} diff --git a/lib/core/utils/pwa_prompt_web.dart b/lib/core/utils/pwa_prompt_web.dart new file mode 100644 index 0000000..fc5eedd --- /dev/null +++ b/lib/core/utils/pwa_prompt_web.dart @@ -0,0 +1,15 @@ +import 'dart:js_interop'; + +@JS('window._schedulifyShowPwaPrompt') +external JSBoolean? get _showPwaPrompt; + +/// Web implementation: reads the flag set by the inline JS in index.html. +class PwaDetector { + static bool shouldShow() { + try { + return _showPwaPrompt?.toDart ?? false; + } catch (_) { + return false; + } + } +} diff --git a/lib/features/auth/screens/login_screen.dart b/lib/features/auth/screens/login_screen.dart index d587079..94fdab6 100644 --- a/lib/features/auth/screens/login_screen.dart +++ b/lib/features/auth/screens/login_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/providers/auth_provider.dart'; +import '../../../core/utils/pwa_prompt.dart'; import '../../../shared/widgets/widgets.dart'; class LoginScreen extends ConsumerStatefulWidget { @@ -42,6 +43,8 @@ class _LoginScreenState extends ConsumerState { _ => '/student', }; context.go(route); + // Show iOS PWA install hint (no-op on Android native & desktop). + await PwaPrompt.showIfNeeded(context); } } diff --git a/lib/features/student/screens/scanner_screen.dart b/lib/features/student/screens/scanner_screen.dart index 4f90290..ee6539e 100644 --- a/lib/features/student/screens/scanner_screen.dart +++ b/lib/features/student/screens/scanner_screen.dart @@ -1,8 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:permission_handler/permission_handler.dart'; import '../../../core/providers/auth_provider.dart'; import '../../../core/theme/app_theme.dart'; import '../../../models/attendance_models.dart'; @@ -10,6 +8,7 @@ import '../../../services/attendance_service.dart'; import '../../../services/geofence_service.dart'; import '../../../services/qr_hash_service.dart'; import '../../../shared/widgets/widgets.dart'; +import '../widgets/qr_scanner_view.dart'; class ScannerScreen extends ConsumerStatefulWidget { const ScannerScreen({super.key}); @@ -104,56 +103,7 @@ class _ScannerScreenState extends ConsumerState { Future _requestCameraAndScan() async { if (!mounted) return; - - var status = await Permission.camera.status; - if (!status.isGranted) { - status = await Permission.camera.request(); - } - - if (!mounted) return; - - if (status.isPermanentlyDenied) { - setState(() { - _step = _Step.error; - _errorMessage = - 'Camera permission permanently denied.\n\n' - 'Go to Settings → Apps → Schedulify → Permissions → Camera → Allow,\n' - 'then tap Try Again.'; - }); - return; - } - - if (!status.isGranted) { - setState(() { - _step = _Step.error; - _errorMessage = - 'Camera permission is required to scan the QR code.\nTap Try Again to allow it.'; - }); - return; - } - setState(() => _step = _Step.scanning); - - try { - final result = await BarcodeScanner.scan( - options: const ScanOptions(restrictFormat: [BarcodeFormat.qr]), - ); - - if (!mounted) return; - - if (result.type == ResultType.Cancelled) { - Navigator.of(context).pop(); - return; - } - - await _submitAttendance(result.rawContent); - } catch (e) { - if (!mounted) return; - setState(() { - _step = _Step.error; - _errorMessage = 'Scanner error: $e\n\nTap Try Again.'; - }); - } } Future _submitAttendance(String raw) async { @@ -213,11 +163,11 @@ class _ScannerScreenState extends ConsumerState { appBar: AppBar(title: const Text('Mark Attendance')), body: switch (_step) { _Step.checking => _CheckingView(), - _Step.scanning => _ScanningView(), + _Step.scanning => QrScannerView( + onCodeDetected: _submitAttendance), _Step.error => _ErrorView( message: _errorMessage ?? 'Unknown error', - onRetry: _doGeofenceCheck, - showSettings: _errorMessage?.contains('permanently') ?? false), + onRetry: _doGeofenceCheck), _Step.result => _ResultView( success: _success, submitting: _submitting, @@ -242,27 +192,13 @@ class _CheckingView extends StatelessWidget { ); } -class _ScanningView extends StatelessWidget { - @override - Widget build(BuildContext context) => const Center( - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), - SizedBox(height: 24), - Text('Opening camera…', - style: TextStyle(color: AppColors.textPrimary, fontSize: 16)), - SizedBox(height: 8), - Text('Point at the QR code on the faculty screen', - style: TextStyle(color: AppColors.textSecondary, fontSize: 13)), - ]), - ); -} +// _ScanningView is replaced by QrScannerView in qr_scanner_view.dart. +// It is intentionally not defined here. class _ErrorView extends StatelessWidget { final String message; final VoidCallback onRetry; - final bool showSettings; - const _ErrorView({required this.message, required this.onRetry, - this.showSettings = false}); + const _ErrorView({required this.message, required this.onRetry}); @override Widget build(BuildContext context) => Center( @@ -293,14 +229,6 @@ class _ErrorView extends StatelessWidget { width: double.infinity, onPressed: onRetry, ), - if (showSettings) ...[ - const SizedBox(height: 12), - OutlinedButton.icon( - onPressed: openAppSettings, - icon: const Icon(Icons.settings_rounded, size: 16), - label: const Text('Open App Settings'), - ), - ], ]), ), ); diff --git a/lib/features/student/widgets/qr_scanner_view.dart b/lib/features/student/widgets/qr_scanner_view.dart new file mode 100644 index 0000000..0621d82 --- /dev/null +++ b/lib/features/student/widgets/qr_scanner_view.dart @@ -0,0 +1,321 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +/// A self-contained QR scanner view built on [MobileScanner]. +/// +/// Responsibilities owned here (not in the caller): +/// - Camera lifecycle (controller start/stop/dispose) +/// - Inline camera error display +/// - 5-second re-scan cooldown to prevent duplicate firings +/// - Viewfinder overlay with corner brackets +/// +/// The caller only needs to handle [onCodeDetected] with the raw QR string. +class QrScannerView extends StatefulWidget { + /// Called at most once every [cooldown] with the decoded raw QR value. + final ValueChanged onCodeDetected; + + /// How long to lock the scanner after a detection (prevents duplicates). + final Duration cooldown; + + const QrScannerView({ + super.key, + required this.onCodeDetected, + this.cooldown = const Duration(seconds: 5), + }); + + @override + State createState() => _QrScannerViewState(); +} + +class _QrScannerViewState extends State { + late final MobileScannerController _controller; + String? _cameraError; + bool _locked = false; + Timer? _cooldownTimer; + + @override + void initState() { + super.initState(); + _controller = MobileScannerController( + detectionSpeed: DetectionSpeed.normal, + formats: [BarcodeFormat.qrCode], + ); + } + + @override + void dispose() { + _cooldownTimer?.cancel(); + _controller.dispose(); + super.dispose(); + } + + void _onDetect(BarcodeCapture capture) { + if (_locked) return; + final raw = capture.barcodes.firstOrNull?.rawValue; + if (raw == null || raw.isEmpty) return; + + // Lock immediately so no duplicate fires during the cooldown window. + _locked = true; + _cooldownTimer?.cancel(); + _cooldownTimer = Timer(widget.cooldown, () { + if (mounted) setState(() => _locked = false); + }); + + widget.onCodeDetected(raw); + } + + void _onError(MobileScannerException error) { + setState(() { + _cameraError = switch (error.errorCode) { + MobileScannerErrorCode.permissionDenied => + 'Camera permission denied.\nTap Retry to request access.', + MobileScannerErrorCode.unsupported => + 'Camera not supported on this device.', + _ => 'Camera error: ${error.errorCode.name}. Tap Retry.', + }; + }); + } + + @override + Widget build(BuildContext context) { + if (_cameraError != null) { + return _CameraErrorView( + message: _cameraError!, + onRetry: () async { + setState(() => _cameraError = null); + await _controller.start(); + }, + ); + } + + return Stack( + fit: StackFit.expand, + children: [ + // ── Live camera feed ────────────────────────────────────────────── + MobileScanner( + controller: _controller, + fit: BoxFit.cover, + onDetect: _onDetect, + errorBuilder: (context, error, child) { + WidgetsBinding.instance.addPostFrameCallback((_) => _onError(error)); + return child ?? const SizedBox.shrink(); + }, + ), + + // ── Semi-transparent dimming outside the finder box ─────────────── + CustomPaint(painter: _ViewfinderPainter()), + + // ── Corner bracket overlay ──────────────────────────────────────── + const Center(child: _ViewfinderBrackets()), + + // ── Bottom hint bar ─────────────────────────────────────────────── + Align( + alignment: Alignment.bottomCenter, + child: Container( + width: double.infinity, + margin: const EdgeInsets.fromLTRB(24, 0, 24, 44), + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20), + decoration: BoxDecoration( + color: Colors.black.withAlpha(179), + borderRadius: BorderRadius.circular(14), + ), + child: const Text( + 'Point the camera at the QR code\ndisplayed by your faculty', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white, fontSize: 14, height: 1.4), + ), + ), + ), + + // ── Locked indicator (subtle pulse after a detection) ───────────── + if (_locked) + const Positioned( + top: 24, + left: 0, + right: 0, + child: Center( + child: _ScannedChip(), + ), + ), + ], + ); + } +} + +// ── Camera error fallback ───────────────────────────────────────────────────── + +class _CameraErrorView extends StatelessWidget { + final String message; + final VoidCallback onRetry; + const _CameraErrorView({required this.message, required this.onRetry}); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 72, + height: 72, + decoration: BoxDecoration( + color: Colors.red.withAlpha(38), + shape: BoxShape.circle, + ), + child: const Icon(Icons.videocam_off_rounded, + color: Colors.red, size: 36), + ), + const SizedBox(height: 20), + const Text( + 'Camera Unavailable', + style: TextStyle( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.w700), + ), + const SizedBox(height: 10), + Text( + message, + textAlign: TextAlign.center, + style: + const TextStyle(color: Colors.white54, fontSize: 13, height: 1.5), + ), + const SizedBox(height: 28), + OutlinedButton.icon( + onPressed: onRetry, + icon: const Icon(Icons.refresh_rounded, size: 18), + label: const Text('Retry'), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.white, + side: const BorderSide(color: Colors.white30), + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + ), + ], + ), + ), + ); + } +} + +// ── Chip shown briefly after a code is captured ─────────────────────────────── + +class _ScannedChip extends StatelessWidget { + const _ScannedChip(); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.green.shade700.withAlpha(230), + borderRadius: BorderRadius.circular(20), + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.check_circle_outline_rounded, + color: Colors.white, size: 16), + SizedBox(width: 6), + Text('QR detected — processing…', + style: TextStyle(color: Colors.white, fontSize: 13)), + ], + ), + ); + } +} + +// ── Viewfinder dimming painter ──────────────────────────────────────────────── + +class _ViewfinderPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + const boxSize = 240.0; + final cx = size.width / 2; + final cy = size.height / 2; + final rect = Rect.fromCenter( + center: Offset(cx, cy), width: boxSize, height: boxSize); + + final paint = Paint()..color = Colors.black.withAlpha(140); + // Fill everything except the clear window. + final path = Path() + ..addRect(Rect.fromLTWH(0, 0, size.width, size.height)) + ..addRRect(RRect.fromRectAndRadius(rect, const Radius.circular(12))) + ..fillType = PathFillType.evenOdd; + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(_ViewfinderPainter old) => false; +} + +// ── Corner bracket decoration ───────────────────────────────────────────────── + +class _ViewfinderBrackets extends StatelessWidget { + const _ViewfinderBrackets(); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 240, + height: 240, + child: CustomPaint(painter: _BracketPainter()), + ); + } +} + +class _BracketPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + const len = 28.0; + const thickness = 3.5; + const radius = 12.0; + final paint = Paint() + ..color = Colors.white + ..strokeWidth = thickness + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + + final corners = [ + // top-left + [Offset(radius, 0), Offset(0, 0), Offset(0, radius), Offset(len, 0), Offset(0, len)], + // top-right + [ + Offset(size.width - radius, 0), + Offset(size.width, 0), + Offset(size.width, radius), + Offset(size.width - len, 0), + Offset(size.width, len) + ], + // bottom-left + [ + Offset(0, size.height - radius), + Offset(0, size.height), + Offset(radius, size.height), + Offset(0, size.height - len), + Offset(len, size.height) + ], + // bottom-right + [ + Offset(size.width, size.height - radius), + Offset(size.width, size.height), + Offset(size.width - radius, size.height), + Offset(size.width, size.height - len), + Offset(size.width - len, size.height) + ], + ]; + + for (final c in corners) { + canvas.drawLine(c[0], c[2], paint); // arc approximation via two lines + canvas.drawLine(c[3], c[1], paint); + canvas.drawLine(c[1], c[4], paint); + } + } + + @override + bool shouldRepaint(_BracketPainter old) => false; +} diff --git a/pubspec.lock b/pubspec.lock index e7d8c6d..8ba0abd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,14 +73,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.1" - barcode_scan2: - dependency: "direct main" - description: - name: barcode_scan2 - sha256: "9b539b0ce419005c451de66374c79f39801986f1fd7a213e63d948f21487cd69" - url: "https://pub.dev" - source: hosted - version: "4.7.2" boolean_selector: dependency: transitive description: @@ -752,6 +744,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173" + url: "https://pub.dev" + source: hosted + version: "6.0.11" objective_c: dependency: transitive description: @@ -952,14 +952,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" - protobuf: - dependency: transitive - description: - name: protobuf - sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" - url: "https://pub.dev" - source: hosted - version: "6.0.0" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2296a38..c7e7e88 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: # Attendance system geolocator: ^13.0.2 maps_toolkit: ^3.1.0 - barcode_scan2: ^4.3.3 + mobile_scanner: ^6.0.0 qr_flutter: ^4.1.0 crypto: ^3.0.5 flutter_map: ^7.0.2 diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..379a867 --- /dev/null +++ b/vercel.json @@ -0,0 +1,15 @@ +{ + "rewrites": [ + { "source": "/(.*)", "destination": "/index.html" } + ], + "headers": [ + { + "source": "/flutter_bootstrap.js", + "headers": [{ "key": "Cache-Control", "value": "no-cache" }] + }, + { + "source": "/(.*\\.js|.*\\.wasm)", + "headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }] + } + ] +} diff --git a/web/index.html b/web/index.html index 3b3dc1f..620416f 100644 --- a/web/index.html +++ b/web/index.html @@ -1,46 +1,43 @@ - + - - - + + - - - - + + + + + + + + + + + + - schedulify + Schedulify - + diff --git a/web/manifest.json b/web/manifest.json index 6efb1e2..7616508 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,12 +1,13 @@ { - "name": "schedulify", - "short_name": "schedulify", - "start_url": ".", + "name": "Schedulify", + "short_name": "Schedulify", + "description": "Multi-tenant college scheduling platform", + "start_url": "/", + "scope": "/", "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", "orientation": "portrait-primary", + "background_color": "#0D0D14", + "theme_color": "#0D0D14", "prefer_related_applications": false, "icons": [ { From 56c1da0289114a6adb339f18d132523f5d7f6230 Mon Sep 17 00:00:00 2001 From: gloooomed Date: Sun, 7 Jun 2026 11:02:19 +0530 Subject: [PATCH 2/7] tried removing custom controller to avoid race condition during camera permission --- .../student/widgets/qr_scanner_view.dart | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/features/student/widgets/qr_scanner_view.dart b/lib/features/student/widgets/qr_scanner_view.dart index 0621d82..a7b842b 100644 --- a/lib/features/student/widgets/qr_scanner_view.dart +++ b/lib/features/student/widgets/qr_scanner_view.dart @@ -29,24 +29,14 @@ class QrScannerView extends StatefulWidget { } class _QrScannerViewState extends State { - late final MobileScannerController _controller; String? _cameraError; bool _locked = false; Timer? _cooldownTimer; - - @override - void initState() { - super.initState(); - _controller = MobileScannerController( - detectionSpeed: DetectionSpeed.normal, - formats: [BarcodeFormat.qrCode], - ); - } + int _retryKey = 0; @override void dispose() { _cooldownTimer?.cancel(); - _controller.dispose(); super.dispose(); } @@ -82,10 +72,10 @@ class _QrScannerViewState extends State { if (_cameraError != null) { return _CameraErrorView( message: _cameraError!, - onRetry: () async { - setState(() => _cameraError = null); - await _controller.start(); - }, + onRetry: () => setState(() { + _cameraError = null; + _retryKey++; + }), ); } @@ -94,7 +84,7 @@ class _QrScannerViewState extends State { children: [ // ── Live camera feed ────────────────────────────────────────────── MobileScanner( - controller: _controller, + key: ValueKey(_retryKey), fit: BoxFit.cover, onDetect: _onDetect, errorBuilder: (context, error, child) { From 4f9c34db7608676868c30d02c4c68855ae575386 Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Sun, 7 Jun 2026 11:15:46 +0530 Subject: [PATCH 3/7] upgraded iOS deployment target to 16.0 and implement app lifecycle management for QR scanner --- ios/Podfile | 4 +- ios/Runner.xcodeproj/project.pbxproj | 6 +-- .../student/widgets/qr_scanner_view.dart | 54 ++++++++++++++----- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/ios/Podfile b/ios/Podfile index a4f4fc5..864e5b0 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '15.0' +platform :ios, '16.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -41,7 +41,7 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0' end end end diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f7ba98e..7509bc3 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -469,7 +469,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -598,7 +598,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -649,7 +649,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/lib/features/student/widgets/qr_scanner_view.dart b/lib/features/student/widgets/qr_scanner_view.dart index a7b842b..f8d38fc 100644 --- a/lib/features/student/widgets/qr_scanner_view.dart +++ b/lib/features/student/widgets/qr_scanner_view.dart @@ -28,15 +28,43 @@ class QrScannerView extends StatefulWidget { State createState() => _QrScannerViewState(); } -class _QrScannerViewState extends State { +class _QrScannerViewState extends State + with WidgetsBindingObserver { + late final MobileScannerController _controller; String? _cameraError; bool _locked = false; Timer? _cooldownTimer; int _retryKey = 0; + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _controller = MobileScannerController( + detectionSpeed: DetectionSpeed.normal, + formats: [BarcodeFormat.qrCode], + ); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.paused: + case AppLifecycleState.inactive: + case AppLifecycleState.detached: + _controller.stop(); + case AppLifecycleState.resumed: + if (_cameraError == null) _controller.start(); + case AppLifecycleState.hidden: + break; + } + } + @override void dispose() { + WidgetsBinding.instance.removeObserver(this); _cooldownTimer?.cancel(); + _controller.dispose(); super.dispose(); } @@ -45,7 +73,6 @@ class _QrScannerViewState extends State { final raw = capture.barcodes.firstOrNull?.rawValue; if (raw == null || raw.isEmpty) return; - // Lock immediately so no duplicate fires during the cooldown window. _locked = true; _cooldownTimer?.cancel(); _cooldownTimer = Timer(widget.cooldown, () { @@ -56,6 +83,7 @@ class _QrScannerViewState extends State { } void _onError(MobileScannerException error) { + if (!mounted) return; setState(() { _cameraError = switch (error.errorCode) { MobileScannerErrorCode.permissionDenied => @@ -72,19 +100,23 @@ class _QrScannerViewState extends State { if (_cameraError != null) { return _CameraErrorView( message: _cameraError!, - onRetry: () => setState(() { - _cameraError = null; - _retryKey++; - }), + onRetry: () async { + await _controller.stop(); + setState(() { + _cameraError = null; + _retryKey++; + }); + await _controller.start(); + }, ); } return Stack( fit: StackFit.expand, children: [ - // ── Live camera feed ────────────────────────────────────────────── MobileScanner( key: ValueKey(_retryKey), + controller: _controller, fit: BoxFit.cover, onDetect: _onDetect, errorBuilder: (context, error, child) { @@ -93,13 +125,10 @@ class _QrScannerViewState extends State { }, ), - // ── Semi-transparent dimming outside the finder box ─────────────── CustomPaint(painter: _ViewfinderPainter()), - // ── Corner bracket overlay ──────────────────────────────────────── const Center(child: _ViewfinderBrackets()), - // ── Bottom hint bar ─────────────────────────────────────────────── Align( alignment: Alignment.bottomCenter, child: Container( @@ -118,15 +147,12 @@ class _QrScannerViewState extends State { ), ), - // ── Locked indicator (subtle pulse after a detection) ───────────── if (_locked) const Positioned( top: 24, left: 0, right: 0, - child: Center( - child: _ScannedChip(), - ), + child: Center(child: _ScannedChip()), ), ], ); From 54709a618d4e607180351a1f34ae9d5275ec5a90 Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Sun, 7 Jun 2026 11:30:15 +0530 Subject: [PATCH 4/7] chore(ios): update Podfile.lock and Xcode project for iOS 16.0 deployment target --- ios/Podfile.lock | 87 ++++++++++++++++++++++++---- ios/Runner.xcodeproj/project.pbxproj | 18 ++++++ 2 files changed, 94 insertions(+), 11 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 08ac6df..b196380 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,29 +1,94 @@ PODS: - - barcode_scan2 (0.0.1): - - Flutter - - SwiftProtobuf (~> 1.33) - Flutter (1.0.0) - - SwiftProtobuf (1.38.0) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleMLKit/BarcodeScanning (7.0.0): + - GoogleMLKit/MLKitCore + - MLKitBarcodeScanning (~> 6.0.0) + - GoogleMLKit/MLKitCore (7.0.0): + - MLKitCommon (~> 12.0.0) + - GoogleToolboxForMac/Defines (4.2.1) + - GoogleToolboxForMac/Logger (4.2.1): + - GoogleToolboxForMac/Defines (= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (4.2.1)": + - GoogleToolboxForMac/Defines (= 4.2.1) + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMSessionFetcher/Core (3.5.0) + - MLImage (1.0.0-beta6) + - MLKitBarcodeScanning (6.0.0): + - MLKitCommon (~> 12.0) + - MLKitVision (~> 8.0) + - MLKitCommon (12.0.0): + - GoogleDataTransport (~> 10.0) + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GoogleUtilities/Logger (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLKitVision (8.0.0): + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLImage (= 1.0.0-beta6) + - MLKitCommon (~> 12.0) + - mobile_scanner (6.0.2): + - Flutter + - GoogleMLKit/BarcodeScanning (~> 7.0.0) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - PromisesObjC (2.4.0) DEPENDENCIES: - - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - Flutter (from `Flutter`) + - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) SPEC REPOS: trunk: - - SwiftProtobuf + - GoogleDataTransport + - GoogleMLKit + - GoogleToolboxForMac + - GoogleUtilities + - GTMSessionFetcher + - MLImage + - MLKitBarcodeScanning + - MLKitCommon + - MLKitVision + - nanopb + - PromisesObjC EXTERNAL SOURCES: - barcode_scan2: - :path: ".symlinks/plugins/barcode_scan2/ios" Flutter: :path: Flutter + mobile_scanner: + :path: ".symlinks/plugins/mobile_scanner/ios" SPEC CHECKSUMS: - barcode_scan2: 4e4b850b112f4e29017833e4715f36161f987966 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - SwiftProtobuf: d724b5145bfc609d9a49c1e3e3a3dabb07273ffb + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 + GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56 + MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2 + MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d + MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e + mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 -PODFILE CHECKSUM: 5b09406f5533f670a98a35b5bf281f156d27c54b +PODFILE CHECKSUM: 4e77ce038330c35f61080d66874a64a8205c7a01 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 7509bc3..96fa9f4 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -206,6 +206,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 05E86B82B9FAE641430CFEF1 /* [CP] Embed Pods Frameworks */, + B8E52AA392E84EE8AB020919 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -353,6 +354,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + B8E52AA392E84EE8AB020919 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; E1FD91A40105537C143A10E3 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; From 90148226cb919a573873714ea5f6f319bbe8e329 Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Sun, 7 Jun 2026 13:16:45 +0530 Subject: [PATCH 5/7] fix(scanner): Revert mobile_scanner to barcode_scan2 for Android release stability; Add GitHub Action for APK build --- .github/workflows/android_build.yml | 43 +++ ios/Podfile | 1 + ios/Podfile.lock | 87 +---- ios/Runner.xcodeproj/project.pbxproj | 21 +- .../student/screens/scanner_screen.dart | 36 +- .../student/widgets/qr_scanner_view.dart | 337 ------------------ pubspec.lock | 24 +- pubspec.yaml | 2 +- 8 files changed, 106 insertions(+), 445 deletions(-) create mode 100644 .github/workflows/android_build.yml delete mode 100644 lib/features/student/widgets/qr_scanner_view.dart diff --git a/.github/workflows/android_build.yml b/.github/workflows/android_build.yml new file mode 100644 index 0000000..cc6dfb0 --- /dev/null +++ b/.github/workflows/android_build.yml @@ -0,0 +1,43 @@ +name: Build Android APK + +on: + push: + branches: + - main + - ios-pwa + workflow_dispatch: # Allows manual trigger from GitHub UI + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + cache: 'gradle' + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.24.x' # Or whatever stable version is required + channel: 'stable' + cache: true + + - name: Install Dependencies + run: flutter pub get + + - name: Build APK + run: flutter build apk --release --dart-define-from-file=dart_defines.json + + - name: Upload APK Artifact + uses: actions/upload-artifact@v4 + with: + name: schedulify-release-apk + path: build/app/outputs/flutter-apk/app-release.apk + retention-days: 7 diff --git a/ios/Podfile b/ios/Podfile index 864e5b0..4ab0475 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -42,6 +42,7 @@ post_install do |installer| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0' + config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64' end end end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b196380..b4e92dc 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,94 +1,29 @@ PODS: - - Flutter (1.0.0) - - GoogleDataTransport (10.1.0): - - nanopb (~> 3.30910.0) - - PromisesObjC (~> 2.4) - - GoogleMLKit/BarcodeScanning (7.0.0): - - GoogleMLKit/MLKitCore - - MLKitBarcodeScanning (~> 6.0.0) - - GoogleMLKit/MLKitCore (7.0.0): - - MLKitCommon (~> 12.0.0) - - GoogleToolboxForMac/Defines (4.2.1) - - GoogleToolboxForMac/Logger (4.2.1): - - GoogleToolboxForMac/Defines (= 4.2.1) - - "GoogleToolboxForMac/NSData+zlib (4.2.1)": - - GoogleToolboxForMac/Defines (= 4.2.1) - - GoogleUtilities/Environment (8.1.0): - - GoogleUtilities/Privacy - - GoogleUtilities/Logger (8.1.0): - - GoogleUtilities/Environment - - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (8.1.0) - - GoogleUtilities/UserDefaults (8.1.0): - - GoogleUtilities/Logger - - GoogleUtilities/Privacy - - GTMSessionFetcher/Core (3.5.0) - - MLImage (1.0.0-beta6) - - MLKitBarcodeScanning (6.0.0): - - MLKitCommon (~> 12.0) - - MLKitVision (~> 8.0) - - MLKitCommon (12.0.0): - - GoogleDataTransport (~> 10.0) - - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) - - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" - - GoogleUtilities/Logger (~> 8.0) - - GoogleUtilities/UserDefaults (~> 8.0) - - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) - - MLKitVision (8.0.0): - - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) - - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" - - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) - - MLImage (= 1.0.0-beta6) - - MLKitCommon (~> 12.0) - - mobile_scanner (6.0.2): + - barcode_scan2 (0.0.1): - Flutter - - GoogleMLKit/BarcodeScanning (~> 7.0.0) - - nanopb (3.30910.0): - - nanopb/decode (= 3.30910.0) - - nanopb/encode (= 3.30910.0) - - nanopb/decode (3.30910.0) - - nanopb/encode (3.30910.0) - - PromisesObjC (2.4.0) + - SwiftProtobuf (~> 1.33) + - Flutter (1.0.0) + - SwiftProtobuf (1.38.0) DEPENDENCIES: + - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) - Flutter (from `Flutter`) - - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) SPEC REPOS: trunk: - - GoogleDataTransport - - GoogleMLKit - - GoogleToolboxForMac - - GoogleUtilities - - GTMSessionFetcher - - MLImage - - MLKitBarcodeScanning - - MLKitCommon - - MLKitVision - - nanopb - - PromisesObjC + - SwiftProtobuf EXTERNAL SOURCES: + barcode_scan2: + :path: ".symlinks/plugins/barcode_scan2/ios" Flutter: :path: Flutter - mobile_scanner: - :path: ".symlinks/plugins/mobile_scanner/ios" SPEC CHECKSUMS: + barcode_scan2: 4e4b850b112f4e29017833e4715f36161f987966 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 - GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 - GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 - GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 - GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 - MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56 - MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2 - MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d - MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e - mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 - nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + SwiftProtobuf: d724b5145bfc609d9a49c1e3e3a3dabb07273ffb -PODFILE CHECKSUM: 4e77ce038330c35f61080d66874a64a8205c7a01 +PODFILE CHECKSUM: ba4b7dd263cdb42897a2433345744d9e6a9917e4 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 96fa9f4..eb0e849 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -206,7 +206,6 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 05E86B82B9FAE641430CFEF1 /* [CP] Embed Pods Frameworks */, - B8E52AA392E84EE8AB020919 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -354,23 +353,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - B8E52AA392E84EE8AB020919 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; E1FD91A40105537C143A10E3 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -479,6 +461,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -602,6 +585,7 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -659,6 +643,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/lib/features/student/screens/scanner_screen.dart b/lib/features/student/screens/scanner_screen.dart index ee6539e..e938829 100644 --- a/lib/features/student/screens/scanner_screen.dart +++ b/lib/features/student/screens/scanner_screen.dart @@ -8,7 +8,7 @@ import '../../../services/attendance_service.dart'; import '../../../services/geofence_service.dart'; import '../../../services/qr_hash_service.dart'; import '../../../shared/widgets/widgets.dart'; -import '../widgets/qr_scanner_view.dart'; +import 'package:barcode_scan2/barcode_scan2.dart'; class ScannerScreen extends ConsumerStatefulWidget { const ScannerScreen({super.key}); @@ -104,6 +104,34 @@ class _ScannerScreenState extends ConsumerState { Future _requestCameraAndScan() async { if (!mounted) return; setState(() => _step = _Step.scanning); + + try { + final result = await BarcodeScanner.scan( + options: const ScanOptions(restrictFormat: [BarcodeFormat.qr]), + ); + + if (!mounted) return; + + if (result.type == ResultType.Cancelled) { + Navigator.of(context).pop(); + return; + } + + if (result.type == ResultType.Barcode) { + await _submitAttendance(result.rawContent); + } else { + setState(() { + _step = _Step.error; + _errorMessage = 'Could not read barcode. Try again.'; + }); + } + } catch (e) { + if (!mounted) return; + setState(() { + _step = _Step.error; + _errorMessage = 'Scanner error: $e'; + }); + } } Future _submitAttendance(String raw) async { @@ -163,8 +191,7 @@ class _ScannerScreenState extends ConsumerState { appBar: AppBar(title: const Text('Mark Attendance')), body: switch (_step) { _Step.checking => _CheckingView(), - _Step.scanning => QrScannerView( - onCodeDetected: _submitAttendance), + _Step.scanning => const Center(child: CircularProgressIndicator()), _Step.error => _ErrorView( message: _errorMessage ?? 'Unknown error', onRetry: _doGeofenceCheck), @@ -192,8 +219,7 @@ class _CheckingView extends StatelessWidget { ); } -// _ScanningView is replaced by QrScannerView in qr_scanner_view.dart. -// It is intentionally not defined here. +// QrScannerView is removed, barcode_scan2 handles its own UI class _ErrorView extends StatelessWidget { final String message; diff --git a/lib/features/student/widgets/qr_scanner_view.dart b/lib/features/student/widgets/qr_scanner_view.dart deleted file mode 100644 index f8d38fc..0000000 --- a/lib/features/student/widgets/qr_scanner_view.dart +++ /dev/null @@ -1,337 +0,0 @@ -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; - -/// A self-contained QR scanner view built on [MobileScanner]. -/// -/// Responsibilities owned here (not in the caller): -/// - Camera lifecycle (controller start/stop/dispose) -/// - Inline camera error display -/// - 5-second re-scan cooldown to prevent duplicate firings -/// - Viewfinder overlay with corner brackets -/// -/// The caller only needs to handle [onCodeDetected] with the raw QR string. -class QrScannerView extends StatefulWidget { - /// Called at most once every [cooldown] with the decoded raw QR value. - final ValueChanged onCodeDetected; - - /// How long to lock the scanner after a detection (prevents duplicates). - final Duration cooldown; - - const QrScannerView({ - super.key, - required this.onCodeDetected, - this.cooldown = const Duration(seconds: 5), - }); - - @override - State createState() => _QrScannerViewState(); -} - -class _QrScannerViewState extends State - with WidgetsBindingObserver { - late final MobileScannerController _controller; - String? _cameraError; - bool _locked = false; - Timer? _cooldownTimer; - int _retryKey = 0; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - _controller = MobileScannerController( - detectionSpeed: DetectionSpeed.normal, - formats: [BarcodeFormat.qrCode], - ); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - switch (state) { - case AppLifecycleState.paused: - case AppLifecycleState.inactive: - case AppLifecycleState.detached: - _controller.stop(); - case AppLifecycleState.resumed: - if (_cameraError == null) _controller.start(); - case AppLifecycleState.hidden: - break; - } - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - _cooldownTimer?.cancel(); - _controller.dispose(); - super.dispose(); - } - - void _onDetect(BarcodeCapture capture) { - if (_locked) return; - final raw = capture.barcodes.firstOrNull?.rawValue; - if (raw == null || raw.isEmpty) return; - - _locked = true; - _cooldownTimer?.cancel(); - _cooldownTimer = Timer(widget.cooldown, () { - if (mounted) setState(() => _locked = false); - }); - - widget.onCodeDetected(raw); - } - - void _onError(MobileScannerException error) { - if (!mounted) return; - setState(() { - _cameraError = switch (error.errorCode) { - MobileScannerErrorCode.permissionDenied => - 'Camera permission denied.\nTap Retry to request access.', - MobileScannerErrorCode.unsupported => - 'Camera not supported on this device.', - _ => 'Camera error: ${error.errorCode.name}. Tap Retry.', - }; - }); - } - - @override - Widget build(BuildContext context) { - if (_cameraError != null) { - return _CameraErrorView( - message: _cameraError!, - onRetry: () async { - await _controller.stop(); - setState(() { - _cameraError = null; - _retryKey++; - }); - await _controller.start(); - }, - ); - } - - return Stack( - fit: StackFit.expand, - children: [ - MobileScanner( - key: ValueKey(_retryKey), - controller: _controller, - fit: BoxFit.cover, - onDetect: _onDetect, - errorBuilder: (context, error, child) { - WidgetsBinding.instance.addPostFrameCallback((_) => _onError(error)); - return child ?? const SizedBox.shrink(); - }, - ), - - CustomPaint(painter: _ViewfinderPainter()), - - const Center(child: _ViewfinderBrackets()), - - Align( - alignment: Alignment.bottomCenter, - child: Container( - width: double.infinity, - margin: const EdgeInsets.fromLTRB(24, 0, 24, 44), - padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20), - decoration: BoxDecoration( - color: Colors.black.withAlpha(179), - borderRadius: BorderRadius.circular(14), - ), - child: const Text( - 'Point the camera at the QR code\ndisplayed by your faculty', - textAlign: TextAlign.center, - style: TextStyle(color: Colors.white, fontSize: 14, height: 1.4), - ), - ), - ), - - if (_locked) - const Positioned( - top: 24, - left: 0, - right: 0, - child: Center(child: _ScannedChip()), - ), - ], - ); - } -} - -// ── Camera error fallback ───────────────────────────────────────────────────── - -class _CameraErrorView extends StatelessWidget { - final String message; - final VoidCallback onRetry; - const _CameraErrorView({required this.message, required this.onRetry}); - - @override - Widget build(BuildContext context) { - return Center( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 72, - height: 72, - decoration: BoxDecoration( - color: Colors.red.withAlpha(38), - shape: BoxShape.circle, - ), - child: const Icon(Icons.videocam_off_rounded, - color: Colors.red, size: 36), - ), - const SizedBox(height: 20), - const Text( - 'Camera Unavailable', - style: TextStyle( - color: Colors.white, - fontSize: 17, - fontWeight: FontWeight.w700), - ), - const SizedBox(height: 10), - Text( - message, - textAlign: TextAlign.center, - style: - const TextStyle(color: Colors.white54, fontSize: 13, height: 1.5), - ), - const SizedBox(height: 28), - OutlinedButton.icon( - onPressed: onRetry, - icon: const Icon(Icons.refresh_rounded, size: 18), - label: const Text('Retry'), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, - side: const BorderSide(color: Colors.white30), - padding: - const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - ), - ), - ], - ), - ), - ); - } -} - -// ── Chip shown briefly after a code is captured ─────────────────────────────── - -class _ScannedChip extends StatelessWidget { - const _ScannedChip(); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.green.shade700.withAlpha(230), - borderRadius: BorderRadius.circular(20), - ), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.check_circle_outline_rounded, - color: Colors.white, size: 16), - SizedBox(width: 6), - Text('QR detected — processing…', - style: TextStyle(color: Colors.white, fontSize: 13)), - ], - ), - ); - } -} - -// ── Viewfinder dimming painter ──────────────────────────────────────────────── - -class _ViewfinderPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - const boxSize = 240.0; - final cx = size.width / 2; - final cy = size.height / 2; - final rect = Rect.fromCenter( - center: Offset(cx, cy), width: boxSize, height: boxSize); - - final paint = Paint()..color = Colors.black.withAlpha(140); - // Fill everything except the clear window. - final path = Path() - ..addRect(Rect.fromLTWH(0, 0, size.width, size.height)) - ..addRRect(RRect.fromRectAndRadius(rect, const Radius.circular(12))) - ..fillType = PathFillType.evenOdd; - canvas.drawPath(path, paint); - } - - @override - bool shouldRepaint(_ViewfinderPainter old) => false; -} - -// ── Corner bracket decoration ───────────────────────────────────────────────── - -class _ViewfinderBrackets extends StatelessWidget { - const _ViewfinderBrackets(); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 240, - height: 240, - child: CustomPaint(painter: _BracketPainter()), - ); - } -} - -class _BracketPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - const len = 28.0; - const thickness = 3.5; - const radius = 12.0; - final paint = Paint() - ..color = Colors.white - ..strokeWidth = thickness - ..style = PaintingStyle.stroke - ..strokeCap = StrokeCap.round; - - final corners = [ - // top-left - [Offset(radius, 0), Offset(0, 0), Offset(0, radius), Offset(len, 0), Offset(0, len)], - // top-right - [ - Offset(size.width - radius, 0), - Offset(size.width, 0), - Offset(size.width, radius), - Offset(size.width - len, 0), - Offset(size.width, len) - ], - // bottom-left - [ - Offset(0, size.height - radius), - Offset(0, size.height), - Offset(radius, size.height), - Offset(0, size.height - len), - Offset(len, size.height) - ], - // bottom-right - [ - Offset(size.width, size.height - radius), - Offset(size.width, size.height), - Offset(size.width - radius, size.height), - Offset(size.width, size.height - len), - Offset(size.width - len, size.height) - ], - ]; - - for (final c in corners) { - canvas.drawLine(c[0], c[2], paint); // arc approximation via two lines - canvas.drawLine(c[3], c[1], paint); - canvas.drawLine(c[1], c[4], paint); - } - } - - @override - bool shouldRepaint(_BracketPainter old) => false; -} diff --git a/pubspec.lock b/pubspec.lock index 8ba0abd..e7d8c6d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.1" + barcode_scan2: + dependency: "direct main" + description: + name: barcode_scan2 + sha256: "9b539b0ce419005c451de66374c79f39801986f1fd7a213e63d948f21487cd69" + url: "https://pub.dev" + source: hosted + version: "4.7.2" boolean_selector: dependency: transitive description: @@ -744,14 +752,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - mobile_scanner: - dependency: "direct main" - description: - name: mobile_scanner - sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173" - url: "https://pub.dev" - source: hosted - version: "6.0.11" objective_c: dependency: transitive description: @@ -952,6 +952,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" + url: "https://pub.dev" + source: hosted + version: "6.0.0" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c7e7e88..2296a38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: # Attendance system geolocator: ^13.0.2 maps_toolkit: ^3.1.0 - mobile_scanner: ^6.0.0 + barcode_scan2: ^4.3.3 qr_flutter: ^4.1.0 crypto: ^3.0.5 flutter_map: ^7.0.2 From 9fbdcd2d17bea0878d863b4f1a7e533e5dfda96f Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Sun, 7 Jun 2026 13:21:16 +0530 Subject: [PATCH 6/7] ci: Update Flutter version in GitHub Actions to latest stable to match Dart 3.12 SDK requirement --- .github/workflows/android_build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/android_build.yml b/.github/workflows/android_build.yml index cc6dfb0..f606b42 100644 --- a/.github/workflows/android_build.yml +++ b/.github/workflows/android_build.yml @@ -25,7 +25,6 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.24.x' # Or whatever stable version is required channel: 'stable' cache: true From eb4f673fac4a9185f4a46e29df50a29280a63abd Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Sun, 7 Jun 2026 13:24:15 +0530 Subject: [PATCH 7/7] ci: inject dart_defines.json from GitHub Secrets --- .github/workflows/android_build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/android_build.yml b/.github/workflows/android_build.yml index f606b42..c536989 100644 --- a/.github/workflows/android_build.yml +++ b/.github/workflows/android_build.yml @@ -31,6 +31,9 @@ jobs: - name: Install Dependencies run: flutter pub get + - name: Create dart_defines.json + run: echo '${{ secrets.DART_DEFINES_JSON }}' > dart_defines.json + - name: Build APK run: flutter build apk --release --dart-define-from-file=dart_defines.json