Skip to content

Commit e742ae9

Browse files
authored
Feat/force permission (#2)
* Init flutter * add lint and dart code metrics * add pull request template * Setup Github PR template and Actions * fix lint * Fix github actions * Fix github actions * Add test to stop complaining * Add config object * Add documentation for configs * update docs * update readme * Query for permission status. * Create disclosure page * Fix lint for context across async gaps * request permmissions * add sensor permission * Use multiline text * pop disclosure page after permission granted * rename permission to permissions * save requested status to sharedpref * reorganize folders to match pub.dev package layout convention * Add example dart * Store requested status and don't request again * Show dialog if required permission denied * prevent proceed until permission granted * Hard requirement permission flow * show page if required permission is not granted * do not show permissions that are already granted * add PermissionItemText class * Add permission service status * Show service item * Add show GPS settings page * Add disclosure page test * Take navigatorState instead of Context for API * Add test to main interface api * add flutter force permission test * Add test for disclosure page * format code * format code * fix format * Fix PR check * update code coverage * add code coverage * fix code coverage ci * add codecov badge on readme * format readme * fix unused test * fix links in README
1 parent a74009f commit e742ae9

17 files changed

+1187
-60
lines changed

.github/workflows/on_pr.yaml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ jobs:
1212
channel: 'stable'
1313

1414
- name: Install dependencies
15-
run: flutter pub get
15+
run: |
16+
flutter pub get
17+
flutter pub run build_runner build
1618
1719
- name: Check code is formatted
1820
run: |
@@ -25,5 +27,10 @@ jobs:
2527
- name: Dart Code Metrics Analysis
2628
run: flutter pub run dart_code_metrics:metrics analyze lib --fatal-style --fatal-warnings --fatal-performance --set-exit-on-violation-level=warning
2729

28-
- name: Run tests
29-
run: flutter test
30+
- name: Run tests and generate coverage report
31+
run: flutter test --coverage
32+
33+
- name: Upload coverage to Codecov
34+
uses: codecov/codecov-action@v3
35+
with:
36+
directory: ./coverage

README.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,70 @@
1-
# flutter-force-permission
1+
# flutter-force-permission
2+
[![codecov](https://codecov.io/gh/gogovan/flutter-force-permission/branch/main/graph/badge.svg?token=F9DPJUAVAJ)](https://codecov.io/gh/gogovan/flutter-force-permission)
3+
4+
Show permission disclosure page and allows required permissions and their associated services before the user can proceed.
5+
6+
This package shows a prominent in-app disclosure page for getting permissions as required by [Google Play](https://support.google.com/googleplay/android-developer/answer/9799150?visit_id=638041800350153935-369621111&p=pd-m&rd=1#prominent_disclosure&zippy=%2Cstep-provide-prominent-in-app-disclosure%2Cstep-review-best-practices-for-accessing-location%2Cstep-consider-alternatives-to-accessing-location-in-the-background%2Cstep-make-access-to-location-in-the-background-clear-to-users%2Csee-an-example-of-prominent-in-app-disclosure).
7+
Also support iOS to ensure a consistent experience.
8+
9+
In addition, permissions can be set as "required". If this is set, those required permissions will be required and if users denied it,
10+
this package will show a custom dialog and redirect user to the appropriate settings page provided by the OS.
11+
12+
## Setup
13+
1. Add the following to `pubspec.yaml`
14+
```yaml
15+
dependencies:
16+
flutter_force_permission: ^0.1.0
17+
permission_handler: ^10.2.0
18+
```
19+
2. This package depends on [permission_handler](https://pub.dev/packages/permission_handler). Perform setup according to that package.
20+
3. If any features is required, it is highly recommended to also set the `<uses-feature>` tag in AndroidManifest.xml. Refer to [relevant Android Developers page](https://developer.android.com/guide/topics/manifest/uses-feature-element) for details.
21+
22+
## Usage
23+
1. Create an instance of FlutterForcePermission, providing configuration.
24+
```dart
25+
final perm = FlutterForcePermission(
26+
FlutterForcePermissionConfig(
27+
title: 'Title',
28+
permissionItemConfigs: [
29+
PermissionItemConfig(
30+
permissions: [Permission.locationWhenInUse],
31+
required: true,
32+
itemText: PermissionItemText(
33+
header: 'Foreground Location',
34+
rationaleText: 'Rationale for Foreground location. Required.',
35+
forcedPermissionDialogConfig: ForcedPermissionDialogConfig(
36+
title: 'Please enable location permission',
37+
text: 'Please enable location permission for proper usage.',
38+
buttonText: 'Settings',
39+
),
40+
),
41+
),
42+
PermissionItemConfig(
43+
permissions: [Permission.locationAlways],
44+
itemText: PermissionItemText(
45+
header: 'Background Location',
46+
rationaleText: 'Rationale for Background location. lorem ipsum dolor sit amet.',
47+
),
48+
),
49+
],
50+
),
51+
);
52+
```
53+
2. Show the disclosure page as needed. This method will handle showing the disclosure page and requesting permissions.
54+
This function takes a [NavigatorState](https://api.flutter.dev/flutter/widgets/NavigatorState-class.html) which can be retrieved through `Navigator.of(context)` call.
55+
This is an async function. Wrap the function in an `async` block as needed.
56+
Returns a map of permission and their requested status (granted/denied/etc). Refer to [permission_handler](https://pub.dev/packages/permission_handler) for the interface.
57+
```dart
58+
final result = await perm.show(Navigator.of(context));
59+
```
60+
61+
### Styling
62+
You can set the style of the text shown by setting up a [TextTheme](https://api.flutter.dev/flutter/material/TextTheme-class.html) of the provided context.
63+
- Title uses `headline6` text style.
64+
- Item header use `subtitle1` text style.
65+
- Item body use `bodyText2` text style.
66+
67+
## Issues
68+
69+
## Contributing
70+

analysis_options.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,8 @@ dart_code_metrics:
179179
# weight-of-class: 0.33 # Number of functional public methods divided by the total number of public methods.
180180
metrics-exclude:
181181
- test/**
182-
- lib/views/**
182+
- lib/src/views/**
183183
- lib/main.dart
184-
- lib/models/**
185-
- lib/model_extensions/**
186184
rules:
187185
# Common
188186
- avoid-banned-imports # Configure some imports that you want to ban, ref: https://dartcodemetrics.dev/docs/rules/common/avoid-banned-imports

example/main.dart

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_force_permission/flutter_force_permission.dart';
4+
import 'package:flutter_force_permission/flutter_force_permission_config.dart';
5+
import 'package:flutter_force_permission/forced_permission_dialog_config.dart';
6+
import 'package:flutter_force_permission/permission_item_config.dart';
7+
import 'package:flutter_force_permission/permission_item_text.dart';
8+
import 'package:permission_handler/permission_handler.dart';
9+
10+
/// Example for flutter-force-permission.
11+
///
12+
/// Note that typically you want to show disclosure page when the app is resumed. This can be
13+
/// achieved using [WidgetsBindingObserver].
14+
/// This demo triggers the disclosure page on a button click for simplicity.
15+
Future<void> main() async {
16+
runApp(MaterialApp(
17+
title: 'Flutter Force Permission Demo',
18+
theme: ThemeData(
19+
primarySwatch: Colors.blue,
20+
),
21+
home: const App(),
22+
));
23+
}
24+
25+
class App extends StatelessWidget {
26+
const App({Key? key}) : super(key: key);
27+
28+
@override
29+
Widget build(BuildContext context) {
30+
return Scaffold(
31+
body: Center(
32+
child: ElevatedButton(
33+
child: const Text("Show disclosure page"),
34+
onPressed: () async {
35+
final perm = FlutterForcePermission(
36+
FlutterForcePermissionConfig(
37+
title: 'Title',
38+
confirmText: 'Confirm',
39+
permissionItemConfigs: [
40+
PermissionItemConfig(
41+
permissions: [Permission.notification],
42+
itemText: PermissionItemText(
43+
header: 'Notification',
44+
rationaleText: 'Rationale for Notification.',
45+
icon:
46+
const Icon(Icons.notifications_none, color: Colors.blue),
47+
),
48+
),
49+
PermissionItemConfig(
50+
permissions: [Permission.locationWhenInUse],
51+
required: true,
52+
itemText: PermissionItemText(
53+
header: 'Foreground Location',
54+
rationaleText: 'Rationale for Foreground location. Required.',
55+
icon: const Icon(Icons.location_on_outlined,
56+
color: Colors.blue),
57+
forcedPermissionDialogConfig: ForcedPermissionDialogConfig(
58+
title: 'Please enable location permission',
59+
text: 'Please enable location permission for proper usage.',
60+
buttonText: 'Settings',
61+
),
62+
),
63+
serviceItemText: PermissionItemText(
64+
header: 'GPS',
65+
rationaleText: 'Rationale for GPS. Required.',
66+
forcedPermissionDialogConfig: ForcedPermissionDialogConfig(
67+
title: 'Please enable GPS',
68+
text: 'Please enable GPS for proper usage.',
69+
buttonText: 'Settings',
70+
),
71+
icon: const Icon(Icons.gps_fixed, color: Colors.blue),
72+
),
73+
),
74+
PermissionItemConfig(
75+
permissions: [Permission.locationAlways],
76+
itemText: PermissionItemText(
77+
header: 'Background Location',
78+
rationaleText:
79+
'Rationale for Background location. lorem ipsum dolor sit amet.',
80+
icon: const Icon(Icons.location_on, color: Colors.blue),
81+
),
82+
),
83+
PermissionItemConfig(
84+
permissions: [
85+
Permission.activityRecognition,
86+
Permission.sensors,
87+
],
88+
itemText: PermissionItemText(
89+
header: 'Activity Recognition and sensors',
90+
rationaleText:
91+
'Rationale for Activity Recognition and sensors.',
92+
icon: const Icon(Icons.directions_run, color: Colors.blue),
93+
),
94+
)
95+
],
96+
),
97+
);
98+
99+
final result = await perm.show(Navigator.of(context));
100+
if (kDebugMode) {
101+
print(result);
102+
}
103+
},
104+
)));
105+
}
106+
}

lib/flutter_force_permission.dart

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,102 @@
11
library flutter_force_permission;
22

3-
/// A Calculator.
4-
class Calculator {
5-
/// Returns [value] plus 1.
6-
int addOne(int value) => value + 1;
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_force_permission/flutter_force_permission_config.dart';
5+
import 'package:flutter_force_permission/permission_service_status.dart';
6+
import 'package:flutter_force_permission/src/flutter_force_permission_util.dart';
7+
import 'package:flutter_force_permission/src/permission_service.dart';
8+
import 'package:flutter_force_permission/src/views/disclosure_page.dart';
9+
import 'package:permission_handler/permission_handler.dart';
10+
11+
/// Flutter Force Permission
12+
///
13+
/// Show permission disclosure page and allows required permissions before user can proceed.
14+
class FlutterForcePermission {
15+
/// Constructor. Pass configuration here. Refer to [FlutterForcePermissionConfig] for details.
16+
FlutterForcePermission(this.config) : _service = const TestStub();
17+
18+
@visibleForTesting
19+
FlutterForcePermission.stub(this.config, this._service);
20+
21+
/// Configuration. Refer to [FlutterForcePermissionConfig] for details.
22+
final FlutterForcePermissionConfig config;
23+
24+
final TestStub _service;
25+
26+
/// Show disclosure page.
27+
///
28+
/// This will show the disclosure page according to the provided configuration, and handles requesting permissions.
29+
/// Returns a map of Permission and their status after requesting the permissions.
30+
/// Only permissions specified in the configuration will be included in the return value.
31+
Future<Map<Permission, PermissionServiceStatus>> show(
32+
NavigatorState navigator,
33+
) async {
34+
// Check for permissions.
35+
final permissionStatuses = await getPermissionStatuses();
36+
37+
if (permissionStatuses.values
38+
.every((element) => element.status == PermissionStatus.granted)) {
39+
// All permissions granted, no need to show disclosure page.
40+
return permissionStatuses;
41+
}
42+
43+
final prefs = await _service.getSharedPreference();
44+
var needShow = false;
45+
for (final permConfig in config.permissionItemConfigs) {
46+
for (final perm in permConfig.permissions) {
47+
final requested = prefs.getBool(getRequestedPrefKey(perm));
48+
if (requested != true) {
49+
needShow = true;
50+
break;
51+
}
52+
if (permissionStatuses[perm]?.status != PermissionStatus.granted &&
53+
permConfig.required) {
54+
needShow = true;
55+
break;
56+
}
57+
}
58+
}
59+
60+
if (!needShow) {
61+
return permissionStatuses;
62+
}
63+
64+
// Navigate to disclosure page.
65+
// ignore: avoid-ignoring-return-values, not needed.
66+
await navigator.push(
67+
MaterialPageRoute(
68+
builder: (context) => DisclosurePage(
69+
permissionConfig: config,
70+
permissionStatuses: permissionStatuses,
71+
),
72+
),
73+
);
74+
75+
// Check for permission status again as it is likely updated.
76+
return getPermissionStatuses();
77+
}
78+
79+
/// Get all permission statuses.
80+
///
81+
/// Only permissions specified in the configuration will be queried and returned.
82+
Future<Map<Permission, PermissionServiceStatus>>
83+
getPermissionStatuses() async {
84+
final Map<Permission, PermissionServiceStatus> result = {};
85+
for (final List<Permission> perms
86+
in config.permissionItemConfigs.map((e) => e.permissions)) {
87+
for (final Permission perm in perms) {
88+
final status = await _service.status(perm);
89+
ServiceStatus? serviceStatus;
90+
if (perm is PermissionWithService) {
91+
serviceStatus = await _service.serviceStatus(perm);
92+
}
93+
result[perm] = PermissionServiceStatus(
94+
status: status,
95+
serviceStatus: serviceStatus,
96+
);
97+
}
98+
}
99+
100+
return result;
101+
}
7102
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
library flutter_force_permission;
2+
3+
import 'package:flutter_force_permission/permission_item_config.dart';
4+
5+
/// Configuration for Flutter Force Permission.
6+
class FlutterForcePermissionConfig {
7+
FlutterForcePermissionConfig({
8+
required this.title,
9+
required this.confirmText,
10+
required this.permissionItemConfigs,
11+
});
12+
13+
/// The title for the disclosure page.
14+
final String title;
15+
16+
/// The text for the confirmation button.
17+
final String confirmText;
18+
19+
/// Configurations for requested permissions.
20+
///
21+
/// The list ordering dictates the order of the permissions requested in the disclosure page and the order the OS shows the permission dialogs.
22+
/// See [PermissionItemConfig] for details.
23+
final List<PermissionItemConfig> permissionItemConfigs;
24+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
library flutter_force_permission;
2+
3+
/// Configuration for the dialog shown when requesting users to go to Settings page.
4+
class ForcedPermissionDialogConfig {
5+
ForcedPermissionDialogConfig({
6+
required this.title,
7+
required this.text,
8+
required this.buttonText,
9+
});
10+
11+
/// The title shown for the dialog when requesting users to go to Settings page.
12+
final String title;
13+
14+
/// The body text shown for the dialog when requesting users to go to Settings page.
15+
final String text;
16+
17+
/// The text of the confirm button shown for the dialog when requesting users to go to Settings page.
18+
final String buttonText;
19+
}

0 commit comments

Comments
 (0)