diff --git a/README.md b/README.md index 6f982a4f..4ce749c0 100644 --- a/README.md +++ b/README.md @@ -44,14 +44,14 @@ _Note_ KeyStore was introduced in Android 4.3 (API level 18). The plugin wouldn' ## Platform Implementation 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 -| | read | write | delete | containsKey | readAll | deleteAll | -|---------|--------------------|--------------------|--------------------|--------------------|--------------------|--------------------| -| Android | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| iOS | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Windows | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Linux | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| macOS | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Web | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| | read | write | delete | containsKey | readAll | deleteAll | isCupertinoProtectedDataAvailable | onCupertinoProtectedDataAvailabilityChanged | +| ------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | --------------------------------- | ------------------------------------------- | +| Android | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | +| 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: | +| Windows | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | +| Linux | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | +| 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) | +| Web | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | ## Getting Started diff --git a/flutter_secure_storage/example/lib/main.dart b/flutter_secure_storage/example/lib/main.dart index f5023a49..bdbda172 100644 --- a/flutter_secure_storage/example/lib/main.dart +++ b/flutter_secure_storage/example/lib/main.dart @@ -17,7 +17,7 @@ class ItemsWidget extends StatefulWidget { ItemsWidgetState createState() => ItemsWidgetState(); } -enum _Actions { deleteAll } +enum _Actions { deleteAll, isProtectedDataAvailable } enum _ItemActions { delete, edit, containsKey, read } @@ -56,6 +56,10 @@ class ItemsWidgetState extends State { _readAll(); } + Future _isProtectedDataAvailable() async { + await _storage.isCupertinoProtectedDataAvailable(); + } + Future _addNewItem() async { final String key = _randomValue(); final String value = _randomValue(); @@ -99,6 +103,9 @@ class ItemsWidgetState extends State { case _Actions.deleteAll: _deleteAll(); break; + case _Actions.isProtectedDataAvailable: + _isProtectedDataAvailable(); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -107,6 +114,11 @@ class ItemsWidgetState extends State { value: _Actions.deleteAll, child: Text('Delete all'), ), + const PopupMenuItem( + key: Key('is_protected_data_available'), + value: _Actions.isProtectedDataAvailable, + child: Text('IsProtectedDataAvailable'), + ), ], ), ], diff --git a/flutter_secure_storage/example/test_driver/app_test.dart b/flutter_secure_storage/example/test_driver/app_test.dart index 52e89f1b..813f6b18 100644 --- a/flutter_secure_storage/example/test_driver/app_test.dart +++ b/flutter_secure_storage/example/test_driver/app_test.dart @@ -48,6 +48,10 @@ void main() { await pageObject.rowHasTitle('Row 0', 0); await pageObject.deleteRow(0); await pageObject.hasNoRow(0); + + await Future.delayed(const Duration(seconds: 1)); + + await pageObject.isProtectedDataAvailable(); }, timeout: const Timeout(Duration(seconds: 120)), ); @@ -61,6 +65,8 @@ class HomePageObject { final _addRandomButtonFinder = find.byValueKey('add_random'); final _deleteAllButtonFinder = find.byValueKey('delete_all'); final _popUpMenuButtonFinder = find.byValueKey('popup_menu'); + final _isProtectedDataAvailableButtonFinder = + find.byValueKey('is_protected_data_available'); Future deleteAll() async { await driver.tap(_popUpMenuButtonFinder); @@ -96,4 +102,9 @@ class HomePageObject { Future hasNoRow(int index) async { await driver.waitForAbsent(find.byValueKey('title_row_$index')); } + + Future isProtectedDataAvailable() async { + await driver.tap(_popUpMenuButtonFinder); + await driver.tap(_isProtectedDataAvailableButtonFinder); + } } diff --git a/flutter_secure_storage/ios/Classes/SwiftFlutterSecureStoragePlugin.swift b/flutter_secure_storage/ios/Classes/SwiftFlutterSecureStoragePlugin.swift index 05569034..026c0ef3 100644 --- a/flutter_secure_storage/ios/Classes/SwiftFlutterSecureStoragePlugin.swift +++ b/flutter_secure_storage/ios/Classes/SwiftFlutterSecureStoragePlugin.swift @@ -6,15 +6,20 @@ // import Flutter +import UIKit -public class SwiftFlutterSecureStoragePlugin: NSObject, FlutterPlugin { +public class SwiftFlutterSecureStoragePlugin: NSObject, FlutterPlugin, FlutterStreamHandler { private let flutterSecureStorageManager: FlutterSecureStorage = FlutterSecureStorage() - + private var secStoreAvailabilitySink: FlutterEventSink? + public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "plugins.it_nomads.com/flutter_secure_storage", binaryMessenger: registrar.messenger()) + let eventChannel = FlutterEventChannel(name: "plugins.it_nomads.com/flutter_secure_storage/events", binaryMessenger: registrar.messenger()) let instance = SwiftFlutterSecureStoragePlugin() registrar.addMethodCallDelegate(instance, channel: channel) + registrar.addApplicationDelegate(instance) + eventChannel.setStreamHandler(instance) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -38,11 +43,43 @@ public class SwiftFlutterSecureStoragePlugin: NSObject, FlutterPlugin { self.readAll(call, handleResult) case "containsKey": self.containsKey(call, handleResult) + case "isProtectedDataAvailable": + // UIApplication is not thread safe + DispatchQueue.main.async { + result(UIApplication.shared.isProtectedDataAvailable) + } default: handleResult(FlutterMethodNotImplemented) } } } + + public func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication) { + guard let sink = secStoreAvailabilitySink else { + return + } + + sink(true) + } + + public func applicationProtectedDataWillBecomeUnavailable(_ application: UIApplication) { + guard let sink = secStoreAvailabilitySink else { + return + } + + sink(false) + } + + public func onListen(withArguments arguments: Any?, + eventSink: @escaping FlutterEventSink) -> FlutterError? { + self.secStoreAvailabilitySink = eventSink + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + self.secStoreAvailabilitySink = nil + return nil + } private func read(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { let values = parseCall(call) diff --git a/flutter_secure_storage/lib/flutter_secure_storage.dart b/flutter_secure_storage/lib/flutter_secure_storage.dart index e314e277..1d611063 100644 --- a/flutter_secure_storage/lib/flutter_secure_storage.dart +++ b/flutter_secure_storage/lib/flutter_secure_storage.dart @@ -261,6 +261,22 @@ class FlutterSecureStorage { } } + /// iOS only feature + /// + /// On all unsupported platforms returns an stream emitting `true` once + Stream get onCupertinoProtectedDataAvailabilityChanged => + _platform.onCupertinoProtectedDataAvailabilityChanged; + + /// iOS and macOS only feature. + /// + /// On macOS this is only avaible on macOS 12 or newer. On older versions always returns true. + /// On all unsupported platforms returns true + /// + /// iOS: https://developer.apple.com/documentation/uikit/uiapplication/1622925-isprotecteddataavailable + /// macOS: https://developer.apple.com/documentation/appkit/nsapplication/3752992-isprotecteddataavailable + Future isCupertinoProtectedDataAvailable() => + _platform.isCupertinoProtectedDataAvailable(); + /// Initializes the shared preferences with mock values for testing. @visibleForTesting static void setMockInitialValues(Map values) { diff --git a/flutter_secure_storage/lib/test/test_flutter_secure_storage_platform.dart b/flutter_secure_storage/lib/test/test_flutter_secure_storage_platform.dart index d104ca95..5e7036cc 100644 --- a/flutter_secure_storage/lib/test/test_flutter_secure_storage_platform.dart +++ b/flutter_secure_storage/lib/test/test_flutter_secure_storage_platform.dart @@ -43,4 +43,11 @@ class TestFlutterSecureStoragePlatform extends FlutterSecureStoragePlatform { required Map options, }) async => data[key] = value; + + @override + Future isCupertinoProtectedDataAvailable() => Future.value(true); + + @override + Stream get onCupertinoProtectedDataAvailabilityChanged => + Stream.value(true); } diff --git a/flutter_secure_storage_macos/macos/Classes/FlutterSecureStoragePlugin.swift b/flutter_secure_storage_macos/macos/Classes/FlutterSecureStoragePlugin.swift index bfbd43fb..23cfd9a6 100644 --- a/flutter_secure_storage_macos/macos/Classes/FlutterSecureStoragePlugin.swift +++ b/flutter_secure_storage_macos/macos/Classes/FlutterSecureStoragePlugin.swift @@ -31,6 +31,15 @@ public class FlutterSecureStoragePlugin: NSObject, FlutterPlugin { readAll(call, result) case "containsKey": containsKey(call, result) + case "isProtectedDataAvailable": + // NSApplication is not thread safe + DispatchQueue.main.async { + if #available(macOS 12.0, *) { + result(NSApplication.shared.isProtectedDataAvailable) + } else { + result(true) + } + } default: result(FlutterMethodNotImplemented) } diff --git a/flutter_secure_storage_platform_interface/lib/flutter_secure_storage_platform_interface.dart b/flutter_secure_storage_platform_interface/lib/flutter_secure_storage_platform_interface.dart index 6df30c1d..390b9fbe 100644 --- a/flutter_secure_storage_platform_interface/lib/flutter_secure_storage_platform_interface.dart +++ b/flutter_secure_storage_platform_interface/lib/flutter_secure_storage_platform_interface.dart @@ -1,5 +1,7 @@ library flutter_secure_storage_platform_interface; +import 'dart:io'; + import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -29,6 +31,10 @@ abstract class FlutterSecureStoragePlatform extends PlatformInterface { _instance = instance; } + Stream get onCupertinoProtectedDataAvailabilityChanged; + + Future isCupertinoProtectedDataAvailable(); + Future write({ required String key, required String value, diff --git a/flutter_secure_storage_platform_interface/lib/src/method_channel_flutter_secure_storage.dart b/flutter_secure_storage_platform_interface/lib/src/method_channel_flutter_secure_storage.dart index e76f2171..ccd1c773 100644 --- a/flutter_secure_storage_platform_interface/lib/src/method_channel_flutter_secure_storage.dart +++ b/flutter_secure_storage_platform_interface/lib/src/method_channel_flutter_secure_storage.dart @@ -3,7 +3,25 @@ part of '../flutter_secure_storage_platform_interface.dart'; const MethodChannel _channel = MethodChannel('plugins.it_nomads.com/flutter_secure_storage'); +const EventChannel _eventChannel = + EventChannel('plugins.it_nomads.com/flutter_secure_storage/events'); + class MethodChannelFlutterSecureStorage extends FlutterSecureStoragePlatform { + @override + Stream get onCupertinoProtectedDataAvailabilityChanged => _eventChannel + .receiveBroadcastStream() + .where((event) => event is bool) + .map((event) => event as bool); + + @override + Future isCupertinoProtectedDataAvailable() async { + if (!kIsWeb && Platform.isIOS) { + (await _channel.invokeMethod('isProtectedDataAvailable'))!; + } + + return Future.value(true); + } + @override Future containsKey({ required String key, diff --git a/flutter_secure_storage_platform_interface/test/flutter_secure_storage_platform_interface_mock.dart b/flutter_secure_storage_platform_interface/test/flutter_secure_storage_platform_interface_mock.dart index d0e6ec82..1350a5ed 100644 --- a/flutter_secure_storage_platform_interface/test/flutter_secure_storage_platform_interface_mock.dart +++ b/flutter_secure_storage_platform_interface/test/flutter_secure_storage_platform_interface_mock.dart @@ -50,4 +50,11 @@ class ExtendsFlutterSecureStoragePlatform extends FlutterSecureStoragePlatform { required Map options, }) => Future.value(); + + @override + Future isCupertinoProtectedDataAvailable() => Future.value(true); + + @override + Stream get onCupertinoProtectedDataAvailabilityChanged => + Stream.value(true); } diff --git a/flutter_secure_storage_platform_interface/test/flutter_secure_storage_platform_interface_test.dart b/flutter_secure_storage_platform_interface/test/flutter_secure_storage_platform_interface_test.dart index 5cd52df8..bfcca03c 100644 --- a/flutter_secure_storage_platform_interface/test/flutter_secure_storage_platform_interface_test.dart +++ b/flutter_secure_storage_platform_interface/test/flutter_secure_storage_platform_interface_test.dart @@ -47,6 +47,8 @@ void main() { if (methodCall.method == 'containsKey') { return true; + } else if (methodCall.method == 'isProtectedDataAvailable') { + return true; } return null; @@ -170,5 +172,11 @@ void main() { ], ); }); + + test('isProtectedDataAvailable', () async { + final result = await storage.isCupertinoProtectedDataAvailable(); + + expect(result, true); + }); }); } diff --git a/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_ffi.dart b/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_ffi.dart index 7873b4c7..6ef4d952 100644 --- a/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_ffi.dart +++ b/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_ffi.dart @@ -150,6 +150,13 @@ class FlutterSecureStorageWindows extends FlutterSecureStoragePlatform { _backwardCompatible.delete(key: key, options: options); } } + + @override + Future isCupertinoProtectedDataAvailable() => Future.value(true); + + @override + Stream get onCupertinoProtectedDataAvailabilityChanged => + Stream.value(true); } @visibleForTesting diff --git a/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_stub.dart b/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_stub.dart index c949947c..74f57248 100644 --- a/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_stub.dart +++ b/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_stub.dart @@ -13,13 +13,17 @@ class FlutterSecureStorageWindows extends FlutterSecureStoragePlatform { } @override - Future containsKey( - {required String key, required Map options,}) => + Future containsKey({ + required String key, + required Map options, + }) => Future.value(false); @override - Future delete( - {required String key, required Map options,}) => + Future delete({ + required String key, + required Map options, + }) => Future.value(); @override @@ -27,8 +31,10 @@ class FlutterSecureStorageWindows extends FlutterSecureStoragePlatform { Future.value(); @override - Future read( - {required String key, required Map options,}) => + Future read({ + required String key, + required Map options, + }) => Future.value(); @override @@ -36,9 +42,17 @@ class FlutterSecureStorageWindows extends FlutterSecureStoragePlatform { Future.value({}); @override - Future write( - {required String key, - required String value, - required Map options,}) => + Future write({ + required String key, + required String value, + required Map options, + }) => Future.value(); + + @override + Future isCupertinoProtectedDataAvailable() => Future.value(true); + + @override + Stream get onCupertinoProtectedDataAvailabilityChanged => + Stream.value(true); }