Skip to content

Commit 4529019

Browse files
Merge pull request #629 from bierbaumtim/feature-cupertino-protected-data-availability
Feature - Check availability of protected data on iOS and macOS
2 parents 1a65084 + 3e3993f commit 4529019

13 files changed

+173
-21
lines changed

README.md

+8-8
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ _Note_ KeyStore was introduced in Android 4.3 (API level 18). The plugin wouldn'
4444
## Platform Implementation
4545
Please note that this table represents the functions implemented in this repository and it is possible that changes haven't yet been released on pub.dev
4646

47-
| | read | write | delete | containsKey | readAll | deleteAll |
48-
|---------|--------------------|--------------------|--------------------|--------------------|--------------------|--------------------|
49-
| Android | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
50-
| iOS | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
51-
| Windows | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
52-
| Linux | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
53-
| macOS | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
54-
| Web | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
47+
| | read | write | delete | containsKey | readAll | deleteAll | isCupertinoProtectedDataAvailable | onCupertinoProtectedDataAvailabilityChanged |
48+
| ------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | --------------------------------- | ------------------------------------------- |
49+
| Android | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
50+
| iOS | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
51+
| Windows | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
52+
| Linux | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
53+
| macOS | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: (on macOS 12 and newer) |
54+
| Web | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
5555

5656
## Getting Started
5757

flutter_secure_storage/example/lib/main.dart

+13-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class ItemsWidget extends StatefulWidget {
1717
ItemsWidgetState createState() => ItemsWidgetState();
1818
}
1919

20-
enum _Actions { deleteAll }
20+
enum _Actions { deleteAll, isProtectedDataAvailable }
2121

2222
enum _ItemActions { delete, edit, containsKey, read }
2323

@@ -56,6 +56,10 @@ class ItemsWidgetState extends State<ItemsWidget> {
5656
_readAll();
5757
}
5858

59+
Future<void> _isProtectedDataAvailable() async {
60+
await _storage.isCupertinoProtectedDataAvailable();
61+
}
62+
5963
Future<void> _addNewItem() async {
6064
final String key = _randomValue();
6165
final String value = _randomValue();
@@ -99,6 +103,9 @@ class ItemsWidgetState extends State<ItemsWidget> {
99103
case _Actions.deleteAll:
100104
_deleteAll();
101105
break;
106+
case _Actions.isProtectedDataAvailable:
107+
_isProtectedDataAvailable();
108+
break;
102109
}
103110
},
104111
itemBuilder: (BuildContext context) => <PopupMenuEntry<_Actions>>[
@@ -107,6 +114,11 @@ class ItemsWidgetState extends State<ItemsWidget> {
107114
value: _Actions.deleteAll,
108115
child: Text('Delete all'),
109116
),
117+
const PopupMenuItem(
118+
key: Key('is_protected_data_available'),
119+
value: _Actions.isProtectedDataAvailable,
120+
child: Text('IsProtectedDataAvailable'),
121+
),
110122
],
111123
),
112124
],

flutter_secure_storage/example/test_driver/app_test.dart

+11
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ void main() {
4848
await pageObject.rowHasTitle('Row 0', 0);
4949
await pageObject.deleteRow(0);
5050
await pageObject.hasNoRow(0);
51+
52+
await Future.delayed(const Duration(seconds: 1));
53+
54+
await pageObject.isProtectedDataAvailable();
5155
},
5256
timeout: const Timeout(Duration(seconds: 120)),
5357
);
@@ -61,6 +65,8 @@ class HomePageObject {
6165
final _addRandomButtonFinder = find.byValueKey('add_random');
6266
final _deleteAllButtonFinder = find.byValueKey('delete_all');
6367
final _popUpMenuButtonFinder = find.byValueKey('popup_menu');
68+
final _isProtectedDataAvailableButtonFinder =
69+
find.byValueKey('is_protected_data_available');
6470

6571
Future deleteAll() async {
6672
await driver.tap(_popUpMenuButtonFinder);
@@ -96,4 +102,9 @@ class HomePageObject {
96102
Future hasNoRow(int index) async {
97103
await driver.waitForAbsent(find.byValueKey('title_row_$index'));
98104
}
105+
106+
Future<void> isProtectedDataAvailable() async {
107+
await driver.tap(_popUpMenuButtonFinder);
108+
await driver.tap(_isProtectedDataAvailableButtonFinder);
109+
}
99110
}

flutter_secure_storage/ios/Classes/SwiftFlutterSecureStoragePlugin.swift

+39-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,20 @@
66
//
77

88
import Flutter
9+
import UIKit
910

10-
public class SwiftFlutterSecureStoragePlugin: NSObject, FlutterPlugin {
11+
public class SwiftFlutterSecureStoragePlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
1112

1213
private let flutterSecureStorageManager: FlutterSecureStorage = FlutterSecureStorage()
13-
14+
private var secStoreAvailabilitySink: FlutterEventSink?
15+
1416
public static func register(with registrar: FlutterPluginRegistrar) {
1517
let channel = FlutterMethodChannel(name: "plugins.it_nomads.com/flutter_secure_storage", binaryMessenger: registrar.messenger())
18+
let eventChannel = FlutterEventChannel(name: "plugins.it_nomads.com/flutter_secure_storage/events", binaryMessenger: registrar.messenger())
1619
let instance = SwiftFlutterSecureStoragePlugin()
1720
registrar.addMethodCallDelegate(instance, channel: channel)
21+
registrar.addApplicationDelegate(instance)
22+
eventChannel.setStreamHandler(instance)
1823
}
1924

2025
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
@@ -38,11 +43,43 @@ public class SwiftFlutterSecureStoragePlugin: NSObject, FlutterPlugin {
3843
self.readAll(call, handleResult)
3944
case "containsKey":
4045
self.containsKey(call, handleResult)
46+
case "isProtectedDataAvailable":
47+
// UIApplication is not thread safe
48+
DispatchQueue.main.async {
49+
result(UIApplication.shared.isProtectedDataAvailable)
50+
}
4151
default:
4252
handleResult(FlutterMethodNotImplemented)
4353
}
4454
}
4555
}
56+
57+
public func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication) {
58+
guard let sink = secStoreAvailabilitySink else {
59+
return
60+
}
61+
62+
sink(true)
63+
}
64+
65+
public func applicationProtectedDataWillBecomeUnavailable(_ application: UIApplication) {
66+
guard let sink = secStoreAvailabilitySink else {
67+
return
68+
}
69+
70+
sink(false)
71+
}
72+
73+
public func onListen(withArguments arguments: Any?,
74+
eventSink: @escaping FlutterEventSink) -> FlutterError? {
75+
self.secStoreAvailabilitySink = eventSink
76+
return nil
77+
}
78+
79+
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
80+
self.secStoreAvailabilitySink = nil
81+
return nil
82+
}
4683

4784
private func read(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
4885
let values = parseCall(call)

flutter_secure_storage/lib/flutter_secure_storage.dart

+16
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,22 @@ class FlutterSecureStorage {
261261
}
262262
}
263263

264+
/// iOS only feature
265+
///
266+
/// On all unsupported platforms returns an stream emitting `true` once
267+
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
268+
_platform.onCupertinoProtectedDataAvailabilityChanged;
269+
270+
/// iOS and macOS only feature.
271+
///
272+
/// On macOS this is only avaible on macOS 12 or newer. On older versions always returns true.
273+
/// On all unsupported platforms returns true
274+
///
275+
/// iOS: https://developer.apple.com/documentation/uikit/uiapplication/1622925-isprotecteddataavailable
276+
/// macOS: https://developer.apple.com/documentation/appkit/nsapplication/3752992-isprotecteddataavailable
277+
Future<bool> isCupertinoProtectedDataAvailable() =>
278+
_platform.isCupertinoProtectedDataAvailable();
279+
264280
/// Initializes the shared preferences with mock values for testing.
265281
@visibleForTesting
266282
static void setMockInitialValues(Map<String, String> values) {

flutter_secure_storage/lib/test/test_flutter_secure_storage_platform.dart

+7
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,11 @@ class TestFlutterSecureStoragePlatform extends FlutterSecureStoragePlatform {
4343
required Map<String, String> options,
4444
}) async =>
4545
data[key] = value;
46+
47+
@override
48+
Future<bool> isCupertinoProtectedDataAvailable() => Future.value(true);
49+
50+
@override
51+
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
52+
Stream.value(true);
4653
}

flutter_secure_storage_macos/macos/Classes/FlutterSecureStoragePlugin.swift

+9
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ public class FlutterSecureStoragePlugin: NSObject, FlutterPlugin {
3131
readAll(call, result)
3232
case "containsKey":
3333
containsKey(call, result)
34+
case "isProtectedDataAvailable":
35+
// NSApplication is not thread safe
36+
DispatchQueue.main.async {
37+
if #available(macOS 12.0, *) {
38+
result(NSApplication.shared.isProtectedDataAvailable)
39+
} else {
40+
result(true)
41+
}
42+
}
3443
default:
3544
result(FlutterMethodNotImplemented)
3645
}

flutter_secure_storage_platform_interface/lib/flutter_secure_storage_platform_interface.dart

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
library flutter_secure_storage_platform_interface;
22

3+
import 'dart:io';
4+
35
import 'package:flutter/foundation.dart';
46
import 'package:flutter/services.dart';
57
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
@@ -29,6 +31,10 @@ abstract class FlutterSecureStoragePlatform extends PlatformInterface {
2931
_instance = instance;
3032
}
3133

34+
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged;
35+
36+
Future<bool> isCupertinoProtectedDataAvailable();
37+
3238
Future<void> write({
3339
required String key,
3440
required String value,

flutter_secure_storage_platform_interface/lib/src/method_channel_flutter_secure_storage.dart

+18
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,25 @@ part of '../flutter_secure_storage_platform_interface.dart';
33
const MethodChannel _channel =
44
MethodChannel('plugins.it_nomads.com/flutter_secure_storage');
55

6+
const EventChannel _eventChannel =
7+
EventChannel('plugins.it_nomads.com/flutter_secure_storage/events');
8+
69
class MethodChannelFlutterSecureStorage extends FlutterSecureStoragePlatform {
10+
@override
11+
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged => _eventChannel
12+
.receiveBroadcastStream()
13+
.where((event) => event is bool)
14+
.map((event) => event as bool);
15+
16+
@override
17+
Future<bool> isCupertinoProtectedDataAvailable() async {
18+
if (!kIsWeb && Platform.isIOS) {
19+
(await _channel.invokeMethod<bool>('isProtectedDataAvailable'))!;
20+
}
21+
22+
return Future.value(true);
23+
}
24+
725
@override
826
Future<bool> containsKey({
927
required String key,

flutter_secure_storage_platform_interface/test/flutter_secure_storage_platform_interface_mock.dart

+7
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,11 @@ class ExtendsFlutterSecureStoragePlatform extends FlutterSecureStoragePlatform {
5050
required Map<String, String> options,
5151
}) =>
5252
Future<void>.value();
53+
54+
@override
55+
Future<bool> isCupertinoProtectedDataAvailable() => Future.value(true);
56+
57+
@override
58+
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
59+
Stream.value(true);
5360
}

flutter_secure_storage_platform_interface/test/flutter_secure_storage_platform_interface_test.dart

+8
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ void main() {
4747

4848
if (methodCall.method == 'containsKey') {
4949
return true;
50+
} else if (methodCall.method == 'isProtectedDataAvailable') {
51+
return true;
5052
}
5153

5254
return null;
@@ -170,5 +172,11 @@ void main() {
170172
],
171173
);
172174
});
175+
176+
test('isProtectedDataAvailable', () async {
177+
final result = await storage.isCupertinoProtectedDataAvailable();
178+
179+
expect(result, true);
180+
});
173181
});
174182
}

flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_ffi.dart

+7
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,13 @@ class FlutterSecureStorageWindows extends FlutterSecureStoragePlatform {
150150
_backwardCompatible.delete(key: key, options: options);
151151
}
152152
}
153+
154+
@override
155+
Future<bool> isCupertinoProtectedDataAvailable() => Future.value(true);
156+
157+
@override
158+
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
159+
Stream.value(true);
153160
}
154161

155162
@visibleForTesting

flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_stub.dart

+24-10
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,46 @@ class FlutterSecureStorageWindows extends FlutterSecureStoragePlatform {
1313
}
1414

1515
@override
16-
Future<bool> containsKey(
17-
{required String key, required Map<String, String> options,}) =>
16+
Future<bool> containsKey({
17+
required String key,
18+
required Map<String, String> options,
19+
}) =>
1820
Future.value(false);
1921

2022
@override
21-
Future<void> delete(
22-
{required String key, required Map<String, String> options,}) =>
23+
Future<void> delete({
24+
required String key,
25+
required Map<String, String> options,
26+
}) =>
2327
Future.value();
2428

2529
@override
2630
Future<void> deleteAll({required Map<String, String> options}) =>
2731
Future.value();
2832

2933
@override
30-
Future<String?> read(
31-
{required String key, required Map<String, String> options,}) =>
34+
Future<String?> read({
35+
required String key,
36+
required Map<String, String> options,
37+
}) =>
3238
Future.value();
3339

3440
@override
3541
Future<Map<String, String>> readAll({required Map<String, String> options}) =>
3642
Future.value({});
3743

3844
@override
39-
Future<void> write(
40-
{required String key,
41-
required String value,
42-
required Map<String, String> options,}) =>
45+
Future<void> write({
46+
required String key,
47+
required String value,
48+
required Map<String, String> options,
49+
}) =>
4350
Future.value();
51+
52+
@override
53+
Future<bool> isCupertinoProtectedDataAvailable() => Future.value(true);
54+
55+
@override
56+
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
57+
Stream.value(true);
4458
}

0 commit comments

Comments
 (0)