Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge develop into master #143

Merged
merged 28 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ddc78d6
Fix type error when authenticating with biometrics
JvnSlv Nov 20, 2024
78197a1
Fix encryption breaking the biometrics logic
JvnSlv Nov 20, 2024
d8f3aae
Separate pin verification of pin and biometrics
JvnSlv Nov 21, 2024
739ac6c
Add changelog, update package version
JvnSlv Nov 21, 2024
609aa98
Remove encryption of already encrypted pin
JvnSlv Nov 21, 2024
07cf310
Implement auto biometrics option
JvnSlv Nov 21, 2024
e01e52d
Update readme
JvnSlv Nov 21, 2024
ba9ae5f
Move autoAuth logic to the bloc
JvnSlv Nov 26, 2024
be78943
Revert flutter_svg version
JvnSlv Nov 26, 2024
1bf0b0e
Remove unnecessary method for validating biometrics pin
JvnSlv Dec 2, 2024
7d137ae
Rename autoBiometricAuth to autoPromptBiometric, change bloc logic
JvnSlv Dec 4, 2024
393c3af
Add savePinCodeInSecureStorage function to the pinCodeService
JvnSlv Dec 4, 2024
1112014
Update changelog
JvnSlv Dec 4, 2024
9dd541e
Merge pull request #139 from Prime-Holding/feature/auto-biometrics-op…
JvnSlv Dec 5, 2024
e77eea2
Implement visual and stability fixes
JvnSlv Dec 9, 2024
eb2edd7
Code cleanup
JvnSlv Dec 9, 2024
06cadaa
Code cleanup
JvnSlv Dec 9, 2024
90e4fdf
Code cleanup
JvnSlv Dec 10, 2024
2d83bae
Remove unnecessary try/catch
JvnSlv Dec 10, 2024
5621485
Add pin length check on adding the digit
JvnSlv Dec 10, 2024
25da693
Code cleanup
JvnSlv Dec 10, 2024
36acf89
Code cleanup
JvnSlv Dec 10, 2024
6443970
Merge pull request #141 from Prime-Holding/feature/widget_toolkit_pin…
JvnSlv Dec 10, 2024
90b059c
Removed `isPinCodeInSecureStorage` method from `PinCodeService`. Now …
StanevPrime Dec 10, 2024
045b8f1
Remove the not needed application bar from the example project
StanevPrime Dec 10, 2024
fe05090
Remove the not needed application bar from the example project
StanevPrime Dec 11, 2024
d7f2c19
Improve the changelog texts
StanevPrime Dec 11, 2024
d5145dc
Merge pull request #142 from Prime-Holding/fix/widget-toolkit-pin-rem…
StanevPrime Dec 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/widget_toolkit_pin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## [0.3.0]
Fixes and improvements:
* Fixed a bug where biometrics authentication would not work if encryption was used
* Added biometrics authentication to the example application
* Added `autoPromptBiometric` parameter to `PinCodeKeyboard` to automatically authenticate with biometrics if available

Breaking changes:
* Added `savePinCodeInSecureStorage` to `PinCodeService` for saving the pin code in secure storage
* Removed `isPinCodeInSecureStorage` method from `PinCodeService`. Now this flag is set automatically when the pin code is saved in secure storage and accessed through the `getPinCode` method

## [0.2.2]
* Fixed a bug where a newly input pin could get deleted if the user starts typing again immediately after an error shake animation starts

Expand Down
18 changes: 8 additions & 10 deletions packages/widget_toolkit_pin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,7 @@ class AppPinCodeService implements PinCodeService {
static const _isPinCodeInStorage = 'pinCode';

FlutterSecureStorage get flutterSecureStorage => const FlutterSecureStorage();

@override
Future<bool> isPinCodeInSecureStorage() async {
var isPinCodeInSecureStorage =
await flutterSecureStorage.read(key: _isPinCodeInStorage);

return isPinCodeInSecureStorage != null;
}


@override
Future<String> encryptPinCode(String pinCode) async {
// App specific encryption
Expand Down Expand Up @@ -192,7 +184,7 @@ mapBiometricMessageToString: (message) {

Optionally you can provide `onError` to handle errors out of the package, or to show a notification,
in practice this would only get called if the implementations of `BiometricsLocalDataSource.areBiometricsEnabled()`,
`BiometricsLocalDataSource.setBiometricsEnabled(enable)`,`PinCodeService.isPinCodeInSecureStorage()`,
`BiometricsLocalDataSource.setBiometricsEnabled(enable)`,
`PinCodeService.encryptPinCode()`, `PinCodeService.getPinLength()`, `PinCodeService.verifyPinCode()`,
`PinCodeService.getPinCode()`, throw.

Expand Down Expand Up @@ -251,6 +243,12 @@ to the user when they are prompted to confirm that they want to enable biometri
localizedReason: 'Activate the biometrics of your device',
```

Optionally you can provide `autoPromptBiometric` and set it to true. In this case the biometrics authentication will
be triggered automatically when the keyboard is opened
```dart
autoPromptBiometric: true,
```

## Functional specifications

When the widget is loaded for the first time on the bottom right of the page, there is no button.
Expand Down
47 changes: 21 additions & 26 deletions packages/widget_toolkit_pin/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,28 @@ class MyApp extends StatelessWidget {
theme: ThemeData.light().copyWith(
colorScheme: ColorScheme.fromSwatch(),
extensions: [
PinCodeTheme.light(),
PinCodeTheme.light().copyWith(
pinCodeKeyTextColorPressed: Colors.lightBlue.withOpacity(0.5),
),
WidgetToolkitTheme.light(),
],
),
darkTheme: ThemeData.dark().copyWith(
colorScheme: ColorScheme.fromSwatch(),
extensions: [
PinCodeTheme.dark(),
PinCodeTheme.dark().copyWith(
pinCodeKeyTextColorPressed: Colors.blue[700],
),
WidgetToolkitTheme.dark(),
],
),
home: const MyHomePage(title: 'Widget Toolkit Pin Demo'),
home: const MyHomePage(),
);
}
}

class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});

final String title;
const MyHomePage({super.key});

@override
Widget build(BuildContext context) => MultiProvider(
Expand All @@ -54,10 +56,6 @@ class MyHomePage extends StatelessWidget {
],
child: Builder(
builder: (context) => Scaffold(
appBar: AppBar(
title: Text(title),
),
extendBodyBehindAppBar: true,
body: SizedBox(
height: MediaQuery.of(context).size.height,
child: Column(
Expand Down Expand Up @@ -92,11 +90,14 @@ class MyHomePage extends StatelessWidget {
// or to show a notification, in practice this would only get called if the
// implementations of [BiometricsLocalDataSource.areBiometricsEnabled()],
// [BiometricsLocalDataSource.setBiometricsEnabled(enable)],
// [PinCodeService.isPinCodeInSecureStorage()], [PinCodeService.encryptPinCode()],
// [PinCodeService.encryptPinCode()],
// [PinCodeService.getPinLength()], [PinCodeService.verifyPinCode()],
// [PinCodeService.getPinCode()], throw.
onError: (error, translatedError) =>
_onError(error, translatedError, context),
// Optionally you can provide [autoPromptBiometric] and set it to true.
// In this case the biometric authentication will be triggered automatically
autoPromptBiometric: false,
),
),
],
Expand Down Expand Up @@ -155,19 +156,10 @@ class AppPinCodeService implements PinCodeService {

/// This pin is intended to be stored in the secured storage for production
/// applications
String? _pinCode;

@override
Future<bool> isPinCodeInSecureStorage() async {
if (_pinCode == '1111') {
return Future.value(true);
}
return Future.value(false);
}
final String _pinCode = '1111';

@override
Future<String> encryptPinCode(String pinCode) async {
_pinCode = pinCode;
return Future.value(pinCode);
}

Expand All @@ -176,6 +168,7 @@ class AppPinCodeService implements PinCodeService {

@override
Future<dynamic> verifyPinCode(String pinCode) async {
await Future.delayed(const Duration(seconds: 1));
if (pinCode != '1111') {
throw WrongPinCodeException(pinCode);
}
Expand All @@ -185,11 +178,13 @@ class AppPinCodeService implements PinCodeService {

@override
Future<String?> getPinCode() async {
if (_pinCode == null) {
return Future.value(null);
}
return Future.value(_pinCode);
}

@override
Future<bool> savePinCodeInSecureStorage(String pinCode) async {
return true;
}
}

/// You have to implement and provide a [BiometricsLocalDataSource], you can
Expand All @@ -199,10 +194,10 @@ class ProfileLocalDataSource implements BiometricsLocalDataSource {

/// This bool check is intended to be stored in the secured storage for production
/// applications
bool? _areBiometricsEnabled;
bool _areBiometricsEnabled = true;

@override
Future<bool> areBiometricsEnabled() async => _areBiometricsEnabled ?? false;
Future<bool> areBiometricsEnabled() async => _areBiometricsEnabled;

@override
Future<void> setBiometricsEnabled(bool enable) async =>
Expand Down
22 changes: 11 additions & 11 deletions packages/widget_toolkit_pin/example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,10 @@ packages:
dependency: transitive
description:
name: flutter_svg
sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
sha256: "936d9c1c010d3e234d1672574636f3352b4941ca3decaddd3cafaeb9ad49c471"
url: "https://pub.dev"
source: hosted
version: "2.0.10+1"
version: "2.0.15"
flutter_test:
dependency: "direct dev"
description: flutter
Expand Down Expand Up @@ -606,10 +606,10 @@ packages:
dependency: transitive
description:
name: rx_bloc_list
sha256: "82584d12118a79edaaea05592c5f869010c32add858d663359a455c6af0596c1"
sha256: a23eea42a8ae172a87787c523ec8f9db834629fcd2c3a4c0c1a74cdbaa4606d8
url: "https://pub.dev"
source: hosted
version: "5.0.0"
version: "5.0.1"
rxdart:
dependency: transitive
description:
Expand All @@ -622,10 +622,10 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.3.3"
shared_preferences_android:
dependency: transitive
description:
Expand Down Expand Up @@ -851,10 +851,10 @@ packages:
dependency: transitive
description:
name: vector_graphics
sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
version: "1.1.15"
vector_graphics_codec:
dependency: transitive
description:
Expand All @@ -867,10 +867,10 @@ packages:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
sha256: ab9ff38fc771e9ee1139320adbe3d18a60327370c218c60752068ebee4b49ab1
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
version: "1.1.15"
vector_math:
dependency: transitive
description:
Expand Down Expand Up @@ -939,7 +939,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.2.1"
version: "0.3.0"
xdg_directories:
dependency: transitive
description:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,28 +49,32 @@ class PinCodeBloc extends $PinCodeBloc {
required this.biometricAuthenticationService,
required this.pinCodeService,
required this.localizedReason,
required this.autoPromptBiometric,
}) {
authenticated.connect().addTo(_compositeSubscription);
Rx.merge([
_$addDigitEvent
.asyncMap((digit) async => (
digit: digit,
allowedLength: await pinCodeService.getPinLength()
))
.where((data) => _pinCode.value.length <= data.allowedLength)
.map((data) => _pinCode.value + data.digit),
_$deleteDigitEvent
.map((_) => _pinCode.value.substring(0, _pinCode.value.length - 1)),
]).listen(_pinCode.add).addTo(_compositeSubscription);
}

final PinBiometricsService biometricAuthenticationService;
final PinCodeService pinCodeService;
final String localizedReason;
final bool autoPromptBiometric;

final BehaviorSubject<String> _pinCode = BehaviorSubject.seeded('');

@override
Stream<int> _mapToDigitsCountState() => Rx.merge([
_$addDigitEvent.switchMap((digit) => _addDigit(digit).asResultStream()),
_$deleteDigitEvent.switchMap(
(_) {
_pinCode.add(_pinCode.value.substring(
0, _pinCode.value.isNotEmpty ? _pinCode.value.length - 1 : 0));
return Stream.value(_pinCode.value.length);
},
).asResultStream(),
errorState.mapTo(0).asResultStream(),
]).whereSuccess().startWith(0).share();
Stream<int> _mapToDigitsCountState() =>
_pinCode.map<int>((pinCode) => pinCode.length).share();

@override
Stream<int> _mapToPlaceholderDigitsCountState() => pinCodeService
Expand All @@ -87,10 +91,20 @@ class PinCodeBloc extends $PinCodeBloc {

@override
ConnectableStream<dynamic> _mapToAuthenticatedState() => Rx.merge([
_digitsCountState.switchMap((digitsCount) =>
_checkPin(_pinCode.value, digitsCount).asResultStream()),
digitsCount
.switchMap((_) => pinCodeService.getPinLength().asStream())
.where(
(storedPinLength) => storedPinLength == _pinCode.value.length)
.switchMap((digitsCount) => _checkPin(_pinCode.value)
.asResultStream()
.doOnData((result) => _pinCode.add(''))),
_$biometricsButtonPressedEvent
.switchMap((_) => _authenticateWithBiometrics().asResultStream()),
.mapTo(true)
.startWith(autoPromptBiometric)
.where((shouldPrompt) => shouldPrompt)
.asyncMap((_) => _getAreBiometricsEnabled())
.where((biometricsEnabled) => biometricsEnabled)
.switchMap((_) => _authenticateWithBiometrics().asResultStream())
]).setResultStateHandler(this).whereSuccess().publish();

@override
Expand Down Expand Up @@ -128,50 +142,25 @@ class PinCodeBloc extends $PinCodeBloc {
pinCode != null;
}

/// Adds a digit to the pin code, returning the new length of the pin code
Future<int> _addDigit(String digit) async {
final pinLength = await pinCodeService.getPinLength();
if (_pinCode.value.length < pinLength) {
_pinCode.add(_pinCode.value + digit);
}
return _pinCode.value.length;
}

/// Encrypts and verifies the provided pin code
Future<dynamic> _encryptAndVerify(String pinCode) async {
// Checks the validity of the pin code
Future<dynamic> _checkPin(String pinCode) async {
final encryptedPin = await pinCodeService.encryptPinCode(pinCode);
final verifiedPin = await pinCodeService.verifyPinCode(encryptedPin);
return verifiedPin;
}

/// Checks the validity of the pin code
Future<dynamic> _checkPin(String pinCode, int digits) async {
final storedPinLength = await pinCodeService.getPinLength();
if (storedPinLength != 0 && digits == storedPinLength) {
try {
final authValue = await _encryptAndVerify(pinCode);
final isSaved = await pinCodeService.isPinCodeInSecureStorage();
if (isSaved) {
return authValue;
}
} catch (_) {
_pinCode.value = '';
rethrow;
}
}
return false;
await pinCodeService.savePinCodeInSecureStorage(encryptedPin);
final verificationResult = await pinCodeService.verifyPinCode(encryptedPin);
return verificationResult;
}

/// Authenticates the user with biometrics after which the pin code is
/// retrieved from the device and checked.
Future<bool?> _authenticateWithBiometrics() async {
Future<dynamic> _authenticateWithBiometrics() async {
if (!await biometricAuthenticationService.isDeviceSupported) {
throw ErrorEnableBiometrics(BiometricsMessage.notSupported);
}
if (await biometricAuthenticationService.authenticate(localizedReason)) {
final pinCode = await pinCodeService.getPinCode();
if (pinCode != null) {
return await _checkPin(pinCode, pinCode.length);
final verificationResult = await pinCodeService.verifyPinCode(pinCode);
return verificationResult;
}
}
return false;
Expand Down
Loading
Loading