Skip to content

Commit

Permalink
Merge pull request #110 from Prime-Holding/feat/widget_toolkit_pin-er…
Browse files Browse the repository at this point in the history
…ror-improvements

Feat/widget toolkit pin error improvements
  • Loading branch information
DDavidPrime authored Jul 23, 2024
2 parents 1a28893 + 3ad41fe commit 7a1e6af
Show file tree
Hide file tree
Showing 20 changed files with 224 additions and 174 deletions.
7 changes: 7 additions & 0 deletions packages/widget_toolkit_pin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## [0.1.0]
- Improvements to error handling
- Errors of type `ErrorModel` thrown by `PinCodeService` are now caught by the `PinCodeKeyboard.onError` callback
### Breaking changes:
- Update `PinCodeService.verifyPinCode` to return a `Future<dynamic>` instead of a `bool`
- Update `PinCodeKeyboard.onAuthenticated` signature to accept a `dynamic` parameter passed from `PinCodeService.verifyPinCode` on authentication success

## [0.0.2]
- Upgrade major versions of dependencies: `theme_tailor`, `theme_tailor_annotation`
### Breaking changes:
Expand Down
87 changes: 55 additions & 32 deletions packages/widget_toolkit_pin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ an automatic prompt of a platform dialog that asks you to enable or disable biom

## Diagram

![Pin Biometrics Diagram][diagram]
![Pin Biometrics Diagram][pin_biometrics_diagram]

## Setup

Expand Down Expand Up @@ -92,11 +92,16 @@ class ProfileLocalDataSource implements BiometricsLocalDataSource {

Step 6: Create an implementation of `PinCodeService`. In this example we use double encryption. The
pin code is first encrypted on application level and then the encrypted value is again encrypted on
operating system level, by using the `FlutterSecureStorage` instance. In your implementation, you
are free to choose the types of encryption. In the example two other packages are used:
[encrypt](https://pub.dev/packages/encrypt) and [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage)
In order for the `flutter_secure_storage` plugin to work on your desired platforms, follow the
integration instructions.
the level of the operating system, by using the `FlutterSecureStorage` instance. In your
implementation, you are free to choose the types of encryption. In the example two other packages
are used: [encrypt](https://pub.dev/packages/encrypt) and [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage).

> [!NOTE]
> In order for the `flutter_secure_storage` plugin to work on your desired platforms, follow its integration instructions.
> [!IMPORTANT]
> Any exceptions thrown should inherit from the `ErrorModel` class.
```dart
class AppPinCodeService implements PinCodeService {
static const _isPinCodeInStorage = 'pinCode';
Expand All @@ -106,7 +111,7 @@ class AppPinCodeService implements PinCodeService {
@override
Future<bool> isPinCodeInSecureStorage() async {
var isPinCodeInSecureStorage =
await flutterSecureStorage.read(key: _isPinCodeInStorage);
await flutterSecureStorage.read(key: _isPinCodeInStorage);
return isPinCodeInSecureStorage != null;
}
Expand All @@ -129,17 +134,28 @@ class AppPinCodeService implements PinCodeService {
Future<int> getPinLength() => Future.value(3);
@override
Future<bool> verifyPinCode(String pinCode) async {
Future<dynamic> verifyPinCode(String pinCode) async {
var pinFromStorage =
await flutterSecureStorage.read(key: _isPinCodeInStorage);
return pinCode == pinFromStorage;
await flutterSecureStorage.read(key: _isPinCodeInStorage);
// Throw an exception if the pin code is not in the storage
// or the provided pin code is incorrect
if (pinFromStorage == null || pinFromStorage!=pinCode) {
throw const IncorrectPinCode();
}
// Return the pin code if it is correct
return pinCode;
}
@override
Future<String?> getPinCode() async =>
await flutterSecureStorage.read(key: _isPinCodeInStorage);
}
class IncorrectPinCode extends ErrorModel {
const IncorrectPinCode() : super(errorMessage: 'Incorrect pin code');
}
```

Step 7: Use the `PinCodeKeyboard` widget somewhere in your widget tree, using your implementation of
Expand Down Expand Up @@ -201,10 +217,11 @@ and implementation of the `LocalAuthentication`, `PinBiometricsAuthDataSource`,
addDependencies: false,
```

Optionally you can provide `onAuthenticated` where the function is called
when the user is authenticated.
Optionally you can provide `onAuthenticated` where the function is called when the user is
authenticated (by entering the correct pin or via biometrics). The `onAuthenticated` callback
accepts a dynamic parameter, which is the value returned from the `PinCodeService.verifyPinCode()`.
```dart
onAuthenticated: () {
onAuthenticated: (dynamic result) {
// ...
},
```
Expand Down Expand Up @@ -237,27 +254,33 @@ localizedReason: 'Activate the biometrics of your device',
## Functional specifications

When the widget is loaded for the first time on the bottom right of the page, there is no button.
At this point the biometrics for the app are still not enabled.
After at least 1 number has been selected the delete button shows up. When the length of the input
reached the pin code length the button icon disappears. The pin code is encrypted stored in the
local device secure storage. Then, there is an auto submit of the selected
pin code to the backend for verification. After the pin has been saved successfully in the secure
storage, the biometrics icon appear on the bottom right. When you press it, it triggers enabling
of the biometrics event. The local authentication from the local_auth package is triggered.
The user is asked, if he/she wants to allow the app to use biometrics authentication. When you click
ok, the biometrics authentication is triggered. When it is successful, on the screen is displayed
a message that the biometrics are enabled. The next time when restart the app, because the pin code
will be stored in the device secure storage, the biometrics authentication will be automatically
triggered and the biometrics icon will be displayed on the bottom right. When you press it every
time it will trigger the biometric authentication. If a user types a wrong pin code and the error
ErrorWrongPin is thrown from the service layer, then a shake animation is triggered on the masked
pin code and then the text from the ErrorWrongPin's errorMessage is displayed in the place of the
pin code. Note: If `biometricsLocalDataSource`parameter is not provided to `PinCodeKeyboard` the
biometrics authentication feature cannot be used.
At this point the biometrics for the app are still not enabled. After at least 1 number has been
selected the delete button shows up. When the length of the input reaches the pin code length, the
button icon disappears. The pin code is stored encrypted in the local device secure storage.
Afterwards, the selected pin code is auto submitted to the backend for verification.

After the pin has been saved successfully in the secure storage, the biometrics icon will appear on
the bottom right. Once pressed, it will trigger the enabling of the biometrics event. The local
authentication from the local_auth package is triggered. The user is asked if they want to allow
the app to use biometrics authentication. Once ok is clicked, the biometrics authentication is
triggered. When it is successful, a message is displayed on the screen saying that the biometrics
are enabled.

The next time when the app is restarted, since the pin code is now stored on the device using
secure storage, the biometrics authentication will be automatically triggered and the biometrics
icon will be displayed on the bottom right. When pressed, every next time it will trigger the
biometric authentication. If a user types a wrong pin code and the error is thrown from the service
layer, then a shake animation is triggered on the masked pin code. Any error of the type `ErrorModel`
thrown from the service layer will be displayed in place of the pin code.

> [! NOTE]
> In order for the biometrics authentication feature to be working, the `biometricsLocalDataSource` parameter should be provided to the `PinCodeKeyboard` widget.
---

[ci_badge_lnk]: https://github.com/Prime-Holding/widget_toolkit/workflows/CI/badge.svg
[codecov_badge_lnk]: https://codecov.io/gh/Prime-Holding/widget_toolkit/packages/widget_toolkit/branch/master/graph/badge.svg
[codecov_branch_lnk]: https://codecov.io/gh/Prime-Holding/widget_toolkit/packages/widget_toolkit_pin/branch/master
[code_style_lnk]: https://img.shields.io/badge/style-effective_dart-40c4ff.svg
[license_lnk]: https://img.shields.io/badge/license-MIT-purple.svg
[diagram]: https://raw.githubusercontent.com/Prime-Holding/widget_toolkit/master/packages/widget_toolkit_pin/doc/assets/pin_biometrics_diagram.png
[pin_biometrics_diagram]: https://raw.githubusercontent.com/Prime-Holding/widget_toolkit/master/packages/widget_toolkit_pin/doc/assets/pin_biometrics_diagram.png
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
<string>12.0</string>
</dict>
</plist>
2 changes: 1 addition & 1 deletion packages/widget_toolkit_pin/example/ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '11.0'
# platform :ios, '12.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down
6 changes: 3 additions & 3 deletions packages/widget_toolkit_pin/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ SPEC CHECKSUMS:
app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586

PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011

COCOAPODS: 1.14.3
COCOAPODS: 1.15.2
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
Expand Down Expand Up @@ -342,7 +342,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down Expand Up @@ -420,7 +420,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -469,7 +469,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
47 changes: 29 additions & 18 deletions packages/widget_toolkit_pin/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ class MyHomePage extends StatelessWidget {
addDependencies: true,
// Optionally you can provide [onAuthenticated] where the
// function is invoked when the user is authenticated.
onAuthenticated: () {
// The callback accepts a dynamic value which is the
// result returned from the [PinCodeService.verifyPinCode()]
onAuthenticated: (dynamic result) {
_onAuthenticated(context);
},

Expand Down Expand Up @@ -116,20 +118,18 @@ class MyHomePage extends StatelessWidget {
}

void _onError(Object error, String strValue, BuildContext context) {
if (error is! ErrorWrongPin) {
showBlurredBottomSheet(
context: context,
configuration: const ModalConfiguration(safeAreaBottom: false),
builder: (context) => MessagePanelWidget(
message: _translateError(error),
messageState: MessagePanelState.important,
),
);
}
showBlurredBottomSheet(
context: context,
configuration: const ModalConfiguration(safeAreaBottom: false),
builder: (context) => MessagePanelWidget(
message: _translateError(error),
messageState: MessagePanelState.important,
),
);
}

String _translateError(Object error) =>
error is ErrorWrongPin ? error.errorMessage : 'An error has occurred';
error is ErrorModel ? error.toString() : 'An error has occurred';

String _exampleMapBiometricMessageToString(BiometricsMessage message) {
switch (message) {
Expand Down Expand Up @@ -159,7 +159,7 @@ class AppPinCodeService implements PinCodeService {

@override
Future<bool> isPinCodeInSecureStorage() async {
if (_pinCode == '111') {
if (_pinCode == '1111') {
return Future.value(true);
}
return Future.value(false);
Expand All @@ -172,14 +172,15 @@ class AppPinCodeService implements PinCodeService {
}

@override
Future<int> getPinLength() async => Future.value(3);
Future<int> getPinLength() async => Future.value(4);

@override
Future<bool> verifyPinCode(String pinCode) async {
if (pinCode == '111') {
return Future.value(true);
Future<dynamic> verifyPinCode(String pinCode) async {
if (pinCode != '1111') {
throw WrongPinCodeException(pinCode);
}
return false;

return pinCode;
}

@override
Expand Down Expand Up @@ -207,3 +208,13 @@ class ProfileLocalDataSource implements BiometricsLocalDataSource {
Future<void> setBiometricsEnabled(bool enable) async =>
_areBiometricsEnabled = enable;
}

/// Exception thrown when the pin code is wrong
class WrongPinCodeException implements ErrorModel {
final String pinCode;

WrongPinCodeException(this.pinCode);

@override
String toString() => 'Invalid pin code: $pinCode';
}
Loading

0 comments on commit 7a1e6af

Please sign in to comment.