feat(setup): add android permissions setup wizard and defer notification prompt
This commit is contained in:
1 parent
c4ff024db2
commit
fb1762caf7
5 files changed
+766
-11
No files matched your search
@@ -51,11 +51,13 @@ Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
class FcmService {
|
||||
static bool get _isMobile => !kIsWeb && (Platform.isAndroid || Platform.isIOS);
|
||||
|
||||
static Future<void> initialize() async {
|
||||
static Future<void> initialize({bool requestPermission = false}) async {
|
||||
try {
|
||||
if (!_isMobile || Firebase.apps.isEmpty) return;
|
||||
final messaging = FirebaseMessaging.instance;
|
||||
await messaging.requestPermission(alert: true, badge: true, sound: true);
|
||||
if (requestPermission) {
|
||||
await messaging.requestPermission(alert: true, badge: true, sound: true);
|
||||
}
|
||||
|
||||
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
|
||||
|
||||
@@ -104,6 +106,13 @@ class FcmService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> requestPermission() async {
|
||||
try {
|
||||
if (!_isMobile || Firebase.apps.isEmpty) return;
|
||||
await FirebaseMessaging.instance.requestPermission(alert: true, badge: true, sound: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static Future<void> registerToken() async {
|
||||
try {
|
||||
if (!_isMobile || Firebase.apps.isEmpty) return;
|
||||
|
||||
@@ -12,7 +12,7 @@ class NotificationService {
|
||||
FlutterLocalNotificationsPlugin();
|
||||
bool _initialized = false;
|
||||
|
||||
Future<void> initialize() async {
|
||||
Future<void> initialize({bool requestPermission = false}) async {
|
||||
if (_initialized) return;
|
||||
|
||||
// Skip on Web and Windows (flutter_local_notifications does not support Windows platform interface)
|
||||
@@ -26,9 +26,9 @@ class NotificationService {
|
||||
|
||||
const DarwinInitializationSettings initializationSettingsDarwin =
|
||||
DarwinInitializationSettings(
|
||||
requestAlertPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
requestAlertPermission: false,
|
||||
requestBadgePermission: false,
|
||||
requestSoundPermission: false,
|
||||
);
|
||||
const LinuxInitializationSettings initializationSettingsLinux =
|
||||
LinuxInitializationSettings(defaultActionName: 'Open notification');
|
||||
@@ -104,15 +104,30 @@ class NotificationService {
|
||||
await androidImplementation?.createNotificationChannel(ch);
|
||||
}
|
||||
|
||||
try {
|
||||
await androidImplementation?.requestNotificationsPermission();
|
||||
} catch (_) {}
|
||||
if (requestPermission) {
|
||||
try {
|
||||
await androidImplementation?.requestNotificationsPermission();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
Future<bool> requestPermission() async {
|
||||
if (kIsWeb || !Platform.isAndroid) return true;
|
||||
try {
|
||||
final androidImplementation = _flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>();
|
||||
return await androidImplementation?.requestNotificationsPermission() ?? false;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final Set<String> _recentlyShown = <String>{};
|
||||
|
||||
Future<void> showNotification({
|
||||
|
||||
@@ -31,6 +31,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../core/services/app_update_service.dart';
|
||||
import '../capstone_members/capstone_members_screen.dart';
|
||||
import '../documentation/documentation_screen.dart';
|
||||
import '../setup/setup_wizard_screen.dart';
|
||||
class TitleCaseTextInputFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
||||
@@ -2459,6 +2460,25 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb && Platform.isAndroid)
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const SetupWizardScreen(isFirstTime: false),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.security_rounded, size: 15),
|
||||
label: const Text('Device Permissions'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: isDark ? AppColors.darkTextPrimary : Colors.black87,
|
||||
side: BorderSide(color: isDark ? AppColors.darkBorder : Colors.grey.shade300),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
textStyle: const TextStyle(fontSize: 12.5, fontWeight: FontWeight.w600),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => DocumentationScreen.open(context),
|
||||
icon: const Icon(Icons.menu_book_rounded, size: 15),
|
||||
|
||||
@@ -0,0 +1,682 @@
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../../core/constants/app_colors.dart';
|
||||
import '../../shared/widgets/abstract_background.dart';
|
||||
import '../../shared/widgets/app_button_loader.dart';
|
||||
import '../login/login_screen.dart';
|
||||
import '../../../core/services/fcm_service.dart';
|
||||
import '../../../core/services/notification_service.dart';
|
||||
|
||||
class SetupWizardScreen extends StatefulWidget {
|
||||
final bool isFirstTime;
|
||||
final Widget? nextScreen;
|
||||
final VoidCallback? onComplete;
|
||||
|
||||
const SetupWizardScreen({
|
||||
super.key,
|
||||
this.isFirstTime = true,
|
||||
this.nextScreen,
|
||||
this.onComplete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SetupWizardScreen> createState() => _SetupWizardScreenState();
|
||||
}
|
||||
|
||||
class _SetupWizardScreenState extends State<SetupWizardScreen>
|
||||
with WidgetsBindingObserver {
|
||||
bool _loading = true;
|
||||
bool _isRequestingAll = false;
|
||||
|
||||
bool _storageGranted = false;
|
||||
bool _notificationGranted = false;
|
||||
bool _installPackagesGranted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_checkAllPermissions();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_checkAllPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkAllPermissions() async {
|
||||
if (kIsWeb || !Platform.isAndroid) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_storageGranted = true;
|
||||
_notificationGranted = true;
|
||||
_installPackagesGranted = true;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool storage = false;
|
||||
bool notification = false;
|
||||
bool installPackages = false;
|
||||
|
||||
try {
|
||||
// 1. Storage
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
if (androidInfo.version.sdkInt >= 30) {
|
||||
storage = await Permission.manageExternalStorage.isGranted;
|
||||
} else {
|
||||
storage = await Permission.storage.isGranted;
|
||||
}
|
||||
|
||||
// 2. Notification
|
||||
notification = await Permission.notification.isGranted;
|
||||
|
||||
// 3. Install Unknown Apps
|
||||
installPackages = await Permission.requestInstallPackages.isGranted;
|
||||
} catch (_) {}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_storageGranted = storage;
|
||||
_notificationGranted = notification;
|
||||
_installPackagesGranted = installPackages;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _requestStorage() async {
|
||||
if (kIsWeb || !Platform.isAndroid) return;
|
||||
try {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
PermissionStatus status;
|
||||
if (androidInfo.version.sdkInt >= 30) {
|
||||
status = await Permission.manageExternalStorage.request();
|
||||
} else {
|
||||
status = await Permission.storage.request();
|
||||
}
|
||||
|
||||
if (status.isPermanentlyDenied) {
|
||||
_promptOpenSettings('Storage Access');
|
||||
}
|
||||
} catch (_) {}
|
||||
await _checkAllPermissions();
|
||||
}
|
||||
|
||||
Future<void> _requestNotification() async {
|
||||
if (kIsWeb || !Platform.isAndroid) return;
|
||||
try {
|
||||
final status = await Permission.notification.request();
|
||||
await NotificationService().requestPermission();
|
||||
await FcmService.requestPermission();
|
||||
if (status.isPermanentlyDenied) {
|
||||
_promptOpenSettings('Notifications');
|
||||
}
|
||||
} catch (_) {}
|
||||
await _checkAllPermissions();
|
||||
}
|
||||
|
||||
Future<void> _requestInstallPackages() async {
|
||||
if (kIsWeb || !Platform.isAndroid) return;
|
||||
try {
|
||||
final status = await Permission.requestInstallPackages.request();
|
||||
if (status.isPermanentlyDenied) {
|
||||
_promptOpenSettings('Install Unknown Apps');
|
||||
}
|
||||
} catch (_) {}
|
||||
await _checkAllPermissions();
|
||||
}
|
||||
|
||||
Future<void> _requestAll() async {
|
||||
if (_isRequestingAll) return;
|
||||
setState(() => _isRequestingAll = true);
|
||||
|
||||
if (!_storageGranted) {
|
||||
await _requestStorage();
|
||||
}
|
||||
if (!_notificationGranted) {
|
||||
await _requestNotification();
|
||||
}
|
||||
if (!_installPackagesGranted) {
|
||||
await _requestInstallPackages();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _isRequestingAll = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _promptOpenSettings(String permissionName) {
|
||||
if (!mounted) return;
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
title: Text('$permissionName Required'),
|
||||
content: Text(
|
||||
'$permissionName was permanently denied or requires manual approval in device settings. Please enable it in Application Settings.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
openAppSettings();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Open Settings'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _finishSetup({bool isSkipping = false}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('setup_wizard_completed', true);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (isSkipping && (!_storageGranted || !_notificationGranted || !_installPackagesGranted)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Some features (auto-updates or downloads) may require manual permission later.',
|
||||
),
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.nextScreen != null) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => widget.nextScreen!),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.onComplete != null) {
|
||||
widget.onComplete!();
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.isFirstTime) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
int get _grantedCount {
|
||||
int count = 0;
|
||||
if (_storageGranted) count++;
|
||||
if (_notificationGranted) count++;
|
||||
if (_installPackagesGranted) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
bool get _allGranted => _storageGranted && _notificationGranted && _installPackagesGranted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final cardColor = isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite;
|
||||
final borderColor = isDark ? AppColors.darkBorder : AppColors.borderLight;
|
||||
final primaryTextColor = isDark ? AppColors.darkTextPrimary : AppColors.textPrimary;
|
||||
final secondaryTextColor = isDark ? AppColors.darkTextSecondary : AppColors.textSecondary;
|
||||
|
||||
return Scaffold(
|
||||
body: AbstractBackground(
|
||||
child: SafeArea(
|
||||
child: _loading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.primaryGreen),
|
||||
)
|
||||
: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 620),
|
||||
child: Column(
|
||||
children: [
|
||||
if (!widget.isFirstTime)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, top: 4.0),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// ── Header Crest & Title ───────────────
|
||||
Center(
|
||||
child: Container(
|
||||
width: 76,
|
||||
height: 76,
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColors.primaryGreen.withValues(alpha: 0.3),
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryGreen.withValues(alpha: 0.12),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Image.asset(
|
||||
'assets/images/logo.png',
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) => const Icon(
|
||||
Icons.school_rounded,
|
||||
size: 40,
|
||||
color: AppColors.primaryGreen,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryGreen.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Text(
|
||||
'SETUP WIZARD',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.1,
|
||||
color: AppColors.primaryGreen,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
widget.isFirstTime
|
||||
? 'Welcome to TIS RMS'
|
||||
: 'Device Permissions',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryTextColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Grant the following permissions to enable downloads, real-time sync notifications, and automatic updates on your device.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
height: 1.4,
|
||||
color: secondaryTextColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Progress Card ───────────────────────
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Permissions Progress',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: primaryTextColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$_grantedCount of 3 Granted',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _allGranted
|
||||
? AppColors.primaryGreen
|
||||
: AppColors.warning,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: _grantedCount / 3,
|
||||
minHeight: 6,
|
||||
backgroundColor: isDark
|
||||
? AppColors.darkSurface2
|
||||
: AppColors.borderLight,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
_allGranted
|
||||
? AppColors.primaryGreen
|
||||
: AppColors.warning,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ── Permission Cards ────────────────────
|
||||
_buildPermissionTile(
|
||||
context: context,
|
||||
icon: Icons.folder_open_rounded,
|
||||
iconBgColor: const Color(0xFF2E7D32).withValues(alpha: 0.12),
|
||||
iconColor: const Color(0xFF2E7D32),
|
||||
title: 'Storage Access',
|
||||
subtitle:
|
||||
'Allows downloading student records, Form 137/SF10 documents, report cards, and archives to your device storage.',
|
||||
isGranted: _storageGranted,
|
||||
onRequest: _requestStorage,
|
||||
cardColor: cardColor,
|
||||
borderColor: borderColor,
|
||||
primaryTextColor: primaryTextColor,
|
||||
secondaryTextColor: secondaryTextColor,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildPermissionTile(
|
||||
context: context,
|
||||
icon: Icons.notifications_active_rounded,
|
||||
iconBgColor: const Color(0xFF1976D2).withValues(alpha: 0.12),
|
||||
iconColor: const Color(0xFF1976D2),
|
||||
title: 'Push Notifications',
|
||||
subtitle:
|
||||
'Notifies you in real-time about newly uploaded documents, verification requests, and system alerts even when the app is in the background.',
|
||||
isGranted: _notificationGranted,
|
||||
onRequest: _requestNotification,
|
||||
cardColor: cardColor,
|
||||
borderColor: borderColor,
|
||||
primaryTextColor: primaryTextColor,
|
||||
secondaryTextColor: secondaryTextColor,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildPermissionTile(
|
||||
context: context,
|
||||
icon: Icons.system_update_alt_rounded,
|
||||
iconBgColor: const Color(0xFFE65100).withValues(alpha: 0.12),
|
||||
iconColor: const Color(0xFFE65100),
|
||||
title: 'Install Unknown Apps',
|
||||
subtitle:
|
||||
'Allows in-app auto-updates when newer TIS RMS APK versions are released so you never have to manually reinstall.',
|
||||
isGranted: _installPackagesGranted,
|
||||
onRequest: _requestInstallPackages,
|
||||
cardColor: cardColor,
|
||||
borderColor: borderColor,
|
||||
primaryTextColor: primaryTextColor,
|
||||
secondaryTextColor: secondaryTextColor,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Master Action Button ─────────────────
|
||||
if (!_allGranted) ...[
|
||||
ElevatedButton(
|
||||
onPressed: _isRequestingAll ? null : _requestAll,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: _isRequestingAll
|
||||
? const AppButtonLoader()
|
||||
: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.shield_rounded, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Grant All Permissions',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () => _finishSetup(isSkipping: true),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: secondaryTextColor,
|
||||
),
|
||||
child: Text(
|
||||
widget.isFirstTime
|
||||
? 'Skip for now & Continue'
|
||||
: 'Done',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
ElevatedButton(
|
||||
onPressed: () => _finishSetup(isSkipping: false),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.check_circle_rounded, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.isFirstTime
|
||||
? 'All Set — Continue to TIS RMS'
|
||||
: 'Save & Return',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.arrow_forward_rounded, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: Text(
|
||||
'You can review or change permissions anytime in Android Settings > Apps > TIS RMS.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: secondaryTextColor.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPermissionTile({
|
||||
required BuildContext context,
|
||||
required IconData icon,
|
||||
required Color iconBgColor,
|
||||
required Color iconColor,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required bool isGranted,
|
||||
required VoidCallback onRequest,
|
||||
required Color cardColor,
|
||||
required Color borderColor,
|
||||
required Color primaryTextColor,
|
||||
required Color secondaryTextColor,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: isGranted
|
||||
? AppColors.primaryGreen.withValues(alpha: 0.35)
|
||||
: borderColor,
|
||||
width: isGranted ? 1.5 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: iconBgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 24),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: primaryTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: isGranted
|
||||
? AppColors.primaryGreen.withValues(alpha: 0.12)
|
||||
: Colors.orange.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isGranted ? Icons.check_circle_rounded : Icons.pending_outlined,
|
||||
size: 13,
|
||||
color: isGranted ? AppColors.primaryGreen : Colors.orange,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
isGranted ? 'Granted' : 'Required',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isGranted ? AppColors.primaryGreen : Colors.orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.35,
|
||||
color: secondaryTextColor,
|
||||
),
|
||||
),
|
||||
if (!isGranted) ...[
|
||||
const SizedBox(height: 10),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onRequest,
|
||||
icon: const Icon(Icons.lock_open_rounded, size: 14),
|
||||
label: const Text('Grant Permission'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryGreen,
|
||||
side: const BorderSide(color: AppColors.primaryGreen),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,10 @@ import '../login/login_screen.dart';
|
||||
import '../../layouts/windows_sidebar_layout.dart';
|
||||
import '../../layouts/android_bottom_nav_layout.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../../core/services/foreground_sync_service.dart';
|
||||
import '../../shared/widgets/abstract_background.dart';
|
||||
import '../../shared/widgets/app_button_loader.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../setup/setup_wizard_screen.dart';
|
||||
|
||||
class SplashScreen extends ConsumerStatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
@@ -74,7 +75,6 @@ class _SplashScreenState extends ConsumerState<SplashScreen>
|
||||
}
|
||||
|
||||
Future<void> _initializeApp({bool isRetry = false}) async {
|
||||
await ForegroundSyncService.requestPermissions();
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -86,6 +86,35 @@ class _SplashScreenState extends ConsumerState<SplashScreen>
|
||||
final user = await ref.read(authProvider.notifier).tryAutoLogin();
|
||||
if (!mounted) return;
|
||||
|
||||
// ── Step 3: Check Setup Wizard (Permissions on Android) ────────────────
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (!mounted) return;
|
||||
final wizardCompleted = prefs.getBool('setup_wizard_completed') ?? false;
|
||||
|
||||
final isDesktop = MediaQuery.of(context).size.width >= 800;
|
||||
final targetScreen = user != null
|
||||
? (isDesktop
|
||||
? WindowsSidebarLayout(userRole: user.role)
|
||||
: AndroidBottomNavLayout(userRole: user.role))
|
||||
: const LoginScreen();
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid && !wizardCompleted) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => SetupWizardScreen(
|
||||
isFirstTime: true,
|
||||
nextScreen: targetScreen,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_navigateToNext(user);
|
||||
}
|
||||
|
||||
void _navigateToNext(dynamic user) {
|
||||
if (!mounted) return;
|
||||
if (user != null) {
|
||||
final isDesktop = MediaQuery.of(context).size.width >= 800;
|
||||
Navigator.of(context).pushReplacement(
|
||||
|
||||
Reference in new issue
Block a user