1+ import 'dart:io' show Platform;
2+
13import 'package:flutter/material.dart' ;
24import 'package:permission_handler/permission_handler.dart' ;
35
@@ -23,7 +25,8 @@ class PermissionsPage extends StatefulWidget {
2325 State <PermissionsPage > createState () => _PermissionsPageState ();
2426}
2527
26- class _PermissionsPageState extends State <PermissionsPage > {
28+ class _PermissionsPageState extends State <PermissionsPage >
29+ with WidgetsBindingObserver {
2730 bool _locationGranted = false ;
2831 bool _bluetoothGranted = false ;
2932 bool _locationChecked = false ;
@@ -34,24 +37,47 @@ class _PermissionsPageState extends State<PermissionsPage> {
3437 @override
3538 void initState () {
3639 super .initState ();
40+ WidgetsBinding .instance.addObserver (this );
3741 _checkPermissions ();
3842 }
3943
44+ @override
45+ void dispose () {
46+ WidgetsBinding .instance.removeObserver (this );
47+ super .dispose ();
48+ }
49+
50+ @override
51+ void didChangeAppLifecycleState (AppLifecycleState state) {
52+ // Re-check permissions when user returns from Settings
53+ if (state == AppLifecycleState .resumed) {
54+ _checkPermissions ();
55+ }
56+ }
57+
4058 Future <void > _checkPermissions () async {
4159 try {
4260 final locationStatus = await Permission .locationWhenInUse.status;
43- final btScanStatus = await Permission .bluetoothScan.status;
61+
62+ // iOS uses Permission.bluetooth; Android uses granular BT permissions
63+ PermissionStatus btStatus;
64+ if (Platform .isIOS) {
65+ btStatus = await Permission .bluetooth.status;
66+ } else {
67+ btStatus = await Permission .bluetoothScan.status;
68+ }
4469
4570 if (mounted) {
4671 setState (() {
4772 _locationGranted = locationStatus.isGranted;
48- _bluetoothGranted = btScanStatus .isGranted;
73+ _bluetoothGranted = btStatus .isGranted;
4974 _locationChecked = true ;
5075 _bluetoothChecked = true ;
5176 });
5277 }
53- } catch (_) {
54- // Permission handler may not be available on all platforms.
78+ } catch (e) {
79+ debugPrint ('Permission check error: $e ' );
80+ // Ensure buttons are shown even if check fails
5581 if (mounted) {
5682 setState (() {
5783 _locationChecked = true ;
@@ -61,66 +87,127 @@ class _PermissionsPageState extends State<PermissionsPage> {
6187 }
6288 }
6389
90+ bool _locationRequesting = false ;
91+ bool _bluetoothRequesting = false ;
92+
6493 Future <void > _requestLocation () async {
94+ if (_locationRequesting) return ;
95+ setState (() => _locationRequesting = true );
96+
6597 try {
66- // Check current status first — if permanently denied, open settings
6798 final current = await Permission .locationWhenInUse.status;
99+ debugPrint ('Location permission status before request: $current ' );
100+
68101 if (current.isPermanentlyDenied) {
69102 await openAppSettings ();
103+ if (mounted) setState (() => _locationRequesting = false );
104+ // Re-check after returning from settings
105+ _recheckAfterDelay ();
70106 return ;
71107 }
72108
73109 final status = await Permission .locationWhenInUse.request ();
110+ debugPrint ('Location permission status after request: $status ' );
111+
74112 if (mounted) {
75- setState (() => _locationGranted = status.isGranted);
113+ setState (() {
114+ _locationGranted = status.isGranted;
115+ _locationRequesting = false ;
116+ });
76117 }
118+
77119 if (status.isGranted) {
78120 notifySuccess ();
79121 } else if (status.isPermanentlyDenied) {
80- // User denied with "Don't Allow" — must go to Settings
81- if (mounted) {
82- _showSettingsSnackbar ('Location' );
83- }
122+ if (mounted) _showSettingsSnackbar ('Location' );
84123 }
85124 } catch (e) {
86125 debugPrint ('Location permission error: $e ' );
87- // Fallback: try opening settings directly
126+ if (mounted) setState (() => _locationRequesting = false );
88127 await openAppSettings ();
89128 }
90129 }
91130
92131 Future <void > _requestBluetooth () async {
132+ if (_bluetoothRequesting) return ;
133+ setState (() => _bluetoothRequesting = true );
134+
93135 try {
94- // Check if already permanently denied
95- final current = await Permission .bluetoothScan.status;
96- if (current.isPermanentlyDenied) {
97- await openAppSettings ();
98- return ;
99- }
136+ if (Platform .isIOS) {
137+ // iOS: single bluetooth permission
138+ final current = await Permission .bluetooth.status;
139+ debugPrint ('BT permission status before request (iOS): $current ' );
100140
101- final statuses = await [
102- Permission .bluetoothScan,
103- Permission .bluetoothConnect,
104- Permission .bluetoothAdvertise,
105- ].request ();
141+ if (current.isPermanentlyDenied) {
142+ await openAppSettings ();
143+ if (mounted) setState (() => _bluetoothRequesting = false );
144+ _recheckAfterDelay ();
145+ return ;
146+ }
147+
148+ final status = await Permission .bluetooth.request ();
149+ debugPrint ('BT permission status after request (iOS): $status ' );
106150
107- final allGranted = statuses.values.every ((s) => s.isGranted);
108- if (mounted) {
109- setState (() => _bluetoothGranted = allGranted);
110- }
111- if (allGranted) {
112- notifySuccess ();
113- } else if (statuses.values.any ((s) => s.isPermanentlyDenied)) {
114151 if (mounted) {
115- _showSettingsSnackbar ('Bluetooth' );
152+ setState (() {
153+ _bluetoothGranted = status.isGranted;
154+ _bluetoothRequesting = false ;
155+ });
156+ }
157+
158+ if (status.isGranted) {
159+ notifySuccess ();
160+ } else if (status.isPermanentlyDenied) {
161+ if (mounted) _showSettingsSnackbar ('Bluetooth' );
162+ }
163+ } else {
164+ // Android: granular bluetooth permissions
165+ final current = await Permission .bluetoothScan.status;
166+ debugPrint ('BT permission status before request (Android): $current ' );
167+
168+ if (current.isPermanentlyDenied) {
169+ await openAppSettings ();
170+ if (mounted) setState (() => _bluetoothRequesting = false );
171+ _recheckAfterDelay ();
172+ return ;
173+ }
174+
175+ final statuses = await [
176+ Permission .bluetoothScan,
177+ Permission .bluetoothConnect,
178+ Permission .bluetoothAdvertise,
179+ ].request ();
180+
181+ final allGranted = statuses.values.every ((s) => s.isGranted);
182+ debugPrint ('BT permissions after request (Android): $statuses ' );
183+
184+ if (mounted) {
185+ setState (() {
186+ _bluetoothGranted = allGranted;
187+ _bluetoothRequesting = false ;
188+ });
189+ }
190+
191+ if (allGranted) {
192+ notifySuccess ();
193+ } else if (statuses.values.any ((s) => s.isPermanentlyDenied)) {
194+ if (mounted) _showSettingsSnackbar ('Bluetooth' );
116195 }
117196 }
118197 } catch (e) {
119198 debugPrint ('Bluetooth permission error: $e ' );
199+ if (mounted) setState (() => _bluetoothRequesting = false );
120200 await openAppSettings ();
121201 }
122202 }
123203
204+ /// Re-check permissions after a short delay (user returning from Settings)
205+ void _recheckAfterDelay () {
206+ Future .delayed (const Duration (seconds: 1 ), () {
207+ if (mounted) _checkPermissions ();
208+ });
209+ }
210+
124211 void _showSettingsSnackbar (String permission) {
125212 ScaffoldMessenger .of (context).showSnackBar (
126213 SnackBar (
@@ -180,6 +267,7 @@ class _PermissionsPageState extends State<PermissionsPage> {
180267 'and sharing your location with nearby team members.' ,
181268 isGranted: _locationGranted,
182269 isChecked: _locationChecked,
270+ isRequesting: _locationRequesting,
183271 colors: colors,
184272 onRequest: _requestLocation,
185273 ),
@@ -195,6 +283,7 @@ class _PermissionsPageState extends State<PermissionsPage> {
195283 'discovery and communication with nearby devices.' ,
196284 isGranted: _bluetoothGranted,
197285 isChecked: _bluetoothChecked,
286+ isRequesting: _bluetoothRequesting,
198287 colors: colors,
199288 onRequest: _requestBluetooth,
200289 ),
@@ -236,6 +325,7 @@ class _PermissionCard extends StatelessWidget {
236325 required this .description,
237326 required this .isGranted,
238327 required this .isChecked,
328+ required this .isRequesting,
239329 required this .colors,
240330 required this .onRequest,
241331 });
@@ -245,6 +335,7 @@ class _PermissionCard extends StatelessWidget {
245335 final String description;
246336 final bool isGranted;
247337 final bool isChecked;
338+ final bool isRequesting;
248339 final TacticalColorScheme colors;
249340 final VoidCallback onRequest;
250341
@@ -271,13 +362,28 @@ class _PermissionCard extends StatelessWidget {
271362 Text (description, style: TacticalTextStyles .caption (colors)),
272363 if (isChecked && ! isGranted) ...[
273364 const SizedBox (height: 12 ),
274- TacticalButton (
275- label: 'Grant' ,
276- icon: Icons .shield,
277- colors: colors,
278- isCompact: true ,
279- onPressed: onRequest,
280- ),
365+ if (isRequesting)
366+ SizedBox (
367+ height: 44 ,
368+ child: Center (
369+ child: SizedBox (
370+ width: 24 ,
371+ height: 24 ,
372+ child: CircularProgressIndicator (
373+ strokeWidth: 2 ,
374+ color: colors.accent,
375+ ),
376+ ),
377+ ),
378+ )
379+ else
380+ TacticalButton (
381+ label: 'Grant' ,
382+ icon: Icons .shield,
383+ colors: colors,
384+ isCompact: true ,
385+ onPressed: onRequest,
386+ ),
281387 ],
282388 ],
283389 ),
0 commit comments