Skip to content

Commit

Permalink
Merge pull request #629 from bierbaumtim/feature-cupertino-protected-…
Browse files Browse the repository at this point in the history
…data-availability

Feature - Check availability of protected data on iOS and macOS
  • Loading branch information
juliansteenbakker committed Oct 12, 2023
2 parents 1a65084 + 3e3993f commit 4529019
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 21 deletions.
16 changes: 8 additions & 8 deletions README.md
Expand Up @@ -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

Expand Down
14 changes: 13 additions & 1 deletion flutter_secure_storage/example/lib/main.dart
Expand Up @@ -17,7 +17,7 @@ class ItemsWidget extends StatefulWidget {
ItemsWidgetState createState() => ItemsWidgetState();
}

enum _Actions { deleteAll }
enum _Actions { deleteAll, isProtectedDataAvailable }

enum _ItemActions { delete, edit, containsKey, read }

Expand Down Expand Up @@ -56,6 +56,10 @@ class ItemsWidgetState extends State<ItemsWidget> {
_readAll();
}

Future<void> _isProtectedDataAvailable() async {
await _storage.isCupertinoProtectedDataAvailable();
}

Future<void> _addNewItem() async {
final String key = _randomValue();
final String value = _randomValue();
Expand Down Expand Up @@ -99,6 +103,9 @@ class ItemsWidgetState extends State<ItemsWidget> {
case _Actions.deleteAll:
_deleteAll();
break;
case _Actions.isProtectedDataAvailable:
_isProtectedDataAvailable();
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<_Actions>>[
Expand All @@ -107,6 +114,11 @@ class ItemsWidgetState extends State<ItemsWidget> {
value: _Actions.deleteAll,
child: Text('Delete all'),
),
const PopupMenuItem(
key: Key('is_protected_data_available'),
value: _Actions.isProtectedDataAvailable,
child: Text('IsProtectedDataAvailable'),
),
],
),
],
Expand Down
11 changes: 11 additions & 0 deletions flutter_secure_storage/example/test_driver/app_test.dart
Expand Up @@ -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)),
);
Expand All @@ -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);
Expand Down Expand Up @@ -96,4 +102,9 @@ class HomePageObject {
Future hasNoRow(int index) async {
await driver.waitForAbsent(find.byValueKey('title_row_$index'));
}

Future<void> isProtectedDataAvailable() async {
await driver.tap(_popUpMenuButtonFinder);
await driver.tap(_isProtectedDataAvailableButtonFinder);
}
}
Expand Up @@ -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) {
Expand All @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions flutter_secure_storage/lib/flutter_secure_storage.dart
Expand Up @@ -261,6 +261,22 @@ class FlutterSecureStorage {
}
}

/// iOS only feature
///
/// On all unsupported platforms returns an stream emitting `true` once
Stream<bool> 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<bool> isCupertinoProtectedDataAvailable() =>
_platform.isCupertinoProtectedDataAvailable();

/// Initializes the shared preferences with mock values for testing.
@visibleForTesting
static void setMockInitialValues(Map<String, String> values) {
Expand Down
Expand Up @@ -43,4 +43,11 @@ class TestFlutterSecureStoragePlatform extends FlutterSecureStoragePlatform {
required Map<String, String> options,
}) async =>
data[key] = value;

@override
Future<bool> isCupertinoProtectedDataAvailable() => Future.value(true);

@override
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
Stream.value(true);
}
Expand Up @@ -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)
}
Expand Down
@@ -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';
Expand Down Expand Up @@ -29,6 +31,10 @@ abstract class FlutterSecureStoragePlatform extends PlatformInterface {
_instance = instance;
}

Stream<bool> get onCupertinoProtectedDataAvailabilityChanged;

Future<bool> isCupertinoProtectedDataAvailable();

Future<void> write({
required String key,
required String value,
Expand Down
Expand Up @@ -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<bool> get onCupertinoProtectedDataAvailabilityChanged => _eventChannel
.receiveBroadcastStream()
.where((event) => event is bool)
.map((event) => event as bool);

@override
Future<bool> isCupertinoProtectedDataAvailable() async {
if (!kIsWeb && Platform.isIOS) {
(await _channel.invokeMethod<bool>('isProtectedDataAvailable'))!;
}

return Future.value(true);
}

@override
Future<bool> containsKey({
required String key,
Expand Down
Expand Up @@ -50,4 +50,11 @@ class ExtendsFlutterSecureStoragePlatform extends FlutterSecureStoragePlatform {
required Map<String, String> options,
}) =>
Future<void>.value();

@override
Future<bool> isCupertinoProtectedDataAvailable() => Future.value(true);

@override
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
Stream.value(true);
}
Expand Up @@ -47,6 +47,8 @@ void main() {

if (methodCall.method == 'containsKey') {
return true;
} else if (methodCall.method == 'isProtectedDataAvailable') {
return true;
}

return null;
Expand Down Expand Up @@ -170,5 +172,11 @@ void main() {
],
);
});

test('isProtectedDataAvailable', () async {
final result = await storage.isCupertinoProtectedDataAvailable();

expect(result, true);
});
});
}
Expand Up @@ -150,6 +150,13 @@ class FlutterSecureStorageWindows extends FlutterSecureStoragePlatform {
_backwardCompatible.delete(key: key, options: options);
}
}

@override
Future<bool> isCupertinoProtectedDataAvailable() => Future.value(true);

@override
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
Stream.value(true);
}

@visibleForTesting
Expand Down
Expand Up @@ -13,32 +13,46 @@ class FlutterSecureStorageWindows extends FlutterSecureStoragePlatform {
}

@override
Future<bool> containsKey(
{required String key, required Map<String, String> options,}) =>
Future<bool> containsKey({
required String key,
required Map<String, String> options,
}) =>
Future.value(false);

@override
Future<void> delete(
{required String key, required Map<String, String> options,}) =>
Future<void> delete({
required String key,
required Map<String, String> options,
}) =>
Future.value();

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

@override
Future<String?> read(
{required String key, required Map<String, String> options,}) =>
Future<String?> read({
required String key,
required Map<String, String> options,
}) =>
Future.value();

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

@override
Future<void> write(
{required String key,
required String value,
required Map<String, String> options,}) =>
Future<void> write({
required String key,
required String value,
required Map<String, String> options,
}) =>
Future.value();

@override
Future<bool> isCupertinoProtectedDataAvailable() => Future.value(true);

@override
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
Stream.value(true);
}

0 comments on commit 4529019

Please sign in to comment.