Merge pull request #35 from yaso-meth/Feature-biometrics

Feature-biometrics
This commit is contained in:
yaso-meth
2025-01-14 13:16:28 +02:00
committed by GitHub
25 changed files with 339 additions and 65 deletions

View File

@@ -1,5 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

View File

@@ -1,6 +1,6 @@
package za.co.mzansiinnovationhub.mih
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity: FlutterActivity() {
class MainActivity: FlutterFragmentActivity() {
}

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSFaceIDUsageDescription</key>
<string>Why is my app authenticating using face id?</string>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIFileSharingEnabled</key>

View File

@@ -67,46 +67,48 @@ class _MIHLayoutBuilderState extends State<MIHLayoutBuilder> {
Widget getBody(double width, double height) {
if (widget.pullDownToRefresh == true) {
return LayoutBuilder(builder: (context, BoxConstraints constraints) {
double newheight = constraints.maxHeight;
//print(newheight);
return RefreshIndicator(
onRefresh: widget.onPullDown,
child: ListView.builder(
itemCount: 1,
itemBuilder: (BuildContext context, int index) {
return SafeArea(
child: SizedBox(
width: width,
height: newheight,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(height: 5),
getLayoutHeader(),
const SizedBox(height: 5),
Expanded(child: widget.body),
],
return SafeArea(
child: LayoutBuilder(builder: (context, BoxConstraints constraints) {
double newheight = constraints.maxHeight;
//print(newheight);
return RefreshIndicator(
onRefresh: widget.onPullDown,
child: ListView.builder(
itemCount: 1,
itemBuilder: (BuildContext context, int index) {
return SafeArea(
child: SizedBox(
width: width,
height: newheight,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(height: 5),
getLayoutHeader(),
const SizedBox(height: 5),
Expanded(child: widget.body),
],
),
),
),
);
},
// child: SafeArea(
// child: SizedBox(
// width: width,
// height: height,
// child: Column(
// mainAxisAlignment: MainAxisAlignment.start,
// children: [
// getLayoutHeader(),
// Expanded(child: widget.body),
// ],
// ),
// ),
// ),
),
);
});
);
},
// child: SafeArea(
// child: SizedBox(
// width: width,
// height: height,
// child: Column(
// mainAxisAlignment: MainAxisAlignment.start,
// children: [
// getLayoutHeader(),
// Expanded(child: widget.body),
// ],
// ),
// ),
// ),
),
);
}),
);
} else {
return SafeArea(
child: SizedBox(

View File

@@ -159,3 +159,13 @@ class ClaimStatementGenerationArguments {
this.sig_path,
);
}
class AuthArguments {
final bool personalSelected;
final bool firstBoot;
AuthArguments(
this.personalSelected,
this.firstBoot,
);
}

View File

@@ -1,3 +1,4 @@
import 'package:Mzansi_Innovation_Hub/mih_objects/arguments.dart';
import 'package:flutter/material.dart';
import '../../mih_components/mih_inputs_and_buttons/mih_button.dart';
import '../../mih_components/mih_layout/mih_action.dart';
@@ -42,7 +43,7 @@ class _MIHAboutState extends State<MIHAbout> {
onTap: () {
Navigator.of(context).pushNamedAndRemoveUntil(
'/',
arguments: true,
arguments: AuthArguments(true, false),
(route) => false,
);
},

View File

@@ -1,3 +1,4 @@
import 'package:Mzansi_Innovation_Hub/mih_objects/arguments.dart';
import 'package:flutter/material.dart';
import '../../main.dart';
import '../../mih_apis/mih_api_calls.dart';
@@ -105,7 +106,7 @@ class _PatientAccessRequestState extends State<PatientAccessRequest> {
Navigator.of(context).popAndPushNamed(
'/',
arguments: true,
arguments: AuthArguments(true, false),
);
},
);

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:Mzansi_Innovation_Hub/mih_objects/arguments.dart';
import 'package:flutter/material.dart';
import '../../main.dart';
import 'package:supertokens_flutter/http.dart' as http;
@@ -268,7 +269,7 @@ class _PatientAccessRequestState extends State<Appointments> {
Navigator.of(context).popAndPushNamed(
'/',
arguments: true,
arguments: AuthArguments(true, false),
);
},
);

View File

@@ -1,15 +1,17 @@
import 'package:Mzansi_Innovation_Hub/mih_packages/authentication/biometric_check.dart';
import 'package:flutter/material.dart';
import 'package:supertokens_flutter/supertokens.dart';
import 'package:no_screenshot/no_screenshot.dart';
import '../mih_home/mih_profile_getter.dart';
import 'signin_or_register.dart';
class AuthCheck extends StatefulWidget {
final bool personalSelected;
final bool firstBoot;
const AuthCheck({
super.key,
required this.personalSelected,
required this.firstBoot,
});
@override
@@ -53,8 +55,9 @@ class _AuthCheckState extends State<AuthCheck> {
builder: (context, snapshot) {
//print(snapshot.data);
if (snapshot.data == true) {
return MIHProfileGetter(
return BiometricCheck(
personalSelected: widget.personalSelected,
firstBoot: widget.firstBoot,
);
} else if (snapshot.data == false) {
return const SignInOrRegister();

View File

@@ -0,0 +1,205 @@
import 'package:Mzansi_Innovation_Hub/main.dart';
import 'package:Mzansi_Innovation_Hub/mih_components/mih_inputs_and_buttons/mih_button.dart';
import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_action.dart';
import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_body.dart';
import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_header.dart';
import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_layout_builder.dart';
import 'package:Mzansi_Innovation_Hub/mih_packages/mih_home/mih_profile_getter.dart';
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
import 'package:supertokens_flutter/supertokens.dart';
class BiometricCheck extends StatefulWidget {
final bool personalSelected;
final bool firstBoot;
const BiometricCheck({
super.key,
required this.personalSelected,
required this.firstBoot,
});
@override
State<BiometricCheck> createState() => _BiometricCheckState();
}
class _BiometricCheckState extends State<BiometricCheck> {
bool _isBioAuthenticated = false;
final LocalAuthentication _auth = LocalAuthentication();
MIHAction getActionButton() {
return MIHAction(
icon: Padding(
padding: const EdgeInsets.all(10.0),
child: SizedBox(
height: 50,
child: Image.asset('images/logo_light.png'),
),
),
iconSize: 35,
onTap: () async {
await SuperTokens.signOut(completionHandler: (error) {
print(error);
});
if (await SuperTokens.doesSessionExist() == false) {
Navigator.of(context).popAndPushNamed('/');
}
// Navigator.of(context).pushNamed(
// '/about',
// //arguments: widget.signedInUser,
// );
},
);
}
MIHHeader getHeader() {
return const MIHHeader(
headerAlignment: MainAxisAlignment.center,
headerItems: [
Text(
"",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 25,
),
),
],
);
}
void authenticateUser() async {
final bool canAuthWithBio = await _auth.canCheckBiometrics;
print("Biomentric Available: $canAuthWithBio");
if (canAuthWithBio) {
try {
final bool didBioAuth = await _auth.authenticate(
localizedReason: "Authenticate to access MIH",
options: const AuthenticationOptions(
biometricOnly: false,
),
);
if (didBioAuth) {
setState(() {
_isBioAuthenticated = true;
});
}
print("Authenticated: $didBioAuth");
} catch (error) {
print(error);
}
}
}
MIHBody getBody() {
return MIHBody(
borderOn: false,
bodyItems: [
SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.all(25.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
//logo
Icon(
Icons.fingerprint,
size: 100,
color:
MzanziInnovationHub.of(context)!.theme.secondaryColor(),
),
//spacer
const SizedBox(height: 10),
//Heading
Text(
'Biomentric Authentication',
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.bold,
color: MzanziInnovationHub.of(context)!
.theme
.secondaryColor(),
),
),
//spacer
const SizedBox(height: 25),
if (!_isBioAuthenticated)
Icon(
Icons.lock,
size: 200,
color: MzanziInnovationHub.of(context)!
.theme
.secondaryColor(),
),
if (_isBioAuthenticated)
Icon(
Icons.lock_open,
size: 200,
color: MzanziInnovationHub.of(context)!
.theme
.secondaryColor(),
),
const SizedBox(height: 30),
Container(
alignment: Alignment.bottomCenter,
child: SizedBox(
width: 500.0,
height: 50.0,
child: MIHButton(
buttonText: "Unlock",
buttonColor: MzanziInnovationHub.of(context)!
.theme
.secondaryColor(),
textColor: MzanziInnovationHub.of(context)!
.theme
.primaryColor(),
onTap: () async {
//Check Biometrics
authenticateUser();
},
),
),
),
],
),
),
),
),
],
);
}
Widget getBiomentricAuthScreen() {
return MIHLayoutBuilder(
actionButton: getActionButton(),
header: getHeader(),
secondaryActionButton: null,
body: getBody(),
actionDrawer: null,
secondaryActionDrawer: null,
bottomNavBar: null,
pullDownToRefresh: false,
onPullDown: () async {},
);
}
@override
Widget build(BuildContext context) {
if (MzanziInnovationHub.of(context)!.theme.getPlatform() == "Web") {
return MIHProfileGetter(
personalSelected: widget.personalSelected,
);
} else if (!widget.firstBoot) {
return MIHProfileGetter(
personalSelected: widget.personalSelected,
);
} else {
if (_isBioAuthenticated) {
return MIHProfileGetter(
personalSelected: widget.personalSelected,
);
} else {
return getBiomentricAuthScreen();
}
}
}
}

View File

@@ -89,6 +89,7 @@ class _RegisterState extends State<Register> {
if (response.statusCode == 200) {
var userExists = jsonDecode(response.body);
if (userExists["exists"]) {
Navigator.of(context).pop();
signUpError();
} else {
var response2 = await http.post(

View File

@@ -296,7 +296,7 @@ class _ManageBusinessProfileState extends State<ManageBusinessProfile> {
Navigator.of(context).popAndPushNamed(
'/',
arguments: false,
arguments: AuthArguments(false, false),
);
},
);

View File

@@ -2,6 +2,7 @@ import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_action.dart'
import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_body.dart';
import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_header.dart';
import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_layout_builder.dart';
import 'package:Mzansi_Innovation_Hub/mih_objects/arguments.dart';
import 'package:Mzansi_Innovation_Hub/mih_packages/mih_policy_tos/policy_and_terms_text.dart';
import 'package:flutter/material.dart';
@@ -20,7 +21,7 @@ class _MIHPrivacyPolocyState extends State<MIHPrivacyPolocy> {
onTap: () {
Navigator.of(context).pushNamedAndRemoveUntil(
'/',
arguments: true,
arguments: AuthArguments(true, false),
(route) => false,
);
},

View File

@@ -2,6 +2,7 @@ import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_action.dart'
import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_body.dart';
import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_header.dart';
import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_layout_builder.dart';
import 'package:Mzansi_Innovation_Hub/mih_objects/arguments.dart';
import 'package:Mzansi_Innovation_Hub/mih_packages/mih_policy_tos/policy_and_terms_text.dart';
import 'package:flutter/material.dart';
@@ -18,9 +19,10 @@ class _MIHTermsOfServiceState extends State<MIHTermsOfService> {
icon: const Icon(Icons.arrow_back),
iconSize: 35,
onTap: () {
Navigator.of(context).pushNamed(
Navigator.of(context).pushNamedAndRemoveUntil(
'/',
arguments: true,
arguments: AuthArguments(true, false),
(route) => false,
);
},
);

View File

@@ -247,7 +247,7 @@ class _ProfileUserUpdateState extends State<ProfileUserUpdate> {
Navigator.of(context).popAndPushNamed(
'/',
arguments: true,
arguments: AuthArguments(true, false),
);
},
);

View File

@@ -3,6 +3,7 @@ import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_body.dart';
import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_header.dart';
import 'package:Mzansi_Innovation_Hub/mih_components/mih_layout/mih_layout_builder.dart';
import 'package:Mzansi_Innovation_Hub/mih_objects/app_user.dart';
import 'package:Mzansi_Innovation_Hub/mih_objects/arguments.dart';
import 'package:Mzansi_Innovation_Hub/mih_packages/mzansi_wallet/loyalty_cards.dart';
import 'package:flutter/material.dart';
@@ -28,7 +29,7 @@ class _MzansiWalletState extends State<MzansiWallet> {
Navigator.of(context).pop();
Navigator.of(context).popAndPushNamed(
'/',
arguments: true,
arguments: AuthArguments(true, false),
);
},
);

View File

@@ -532,7 +532,7 @@ class _PatientManagerState extends State<PatientManager> {
Navigator.of(context).popAndPushNamed(
'/',
arguments: false,
arguments: AuthArguments(false, false),
);
},
);

View File

@@ -125,7 +125,7 @@ class _PatientViewState extends State<PatientView> {
Navigator.of(context).pop();
Navigator.of(context).popAndPushNamed(
'/',
arguments: true,
arguments: AuthArguments(true, false),
);
},
);

View File

@@ -54,21 +54,16 @@ class RouteGenerator {
switch (settings.name) {
// Authgentication
case '/':
if (args is bool) {
if (args is AuthArguments) {
return MaterialPageRoute(
settings: settings,
builder: (_) => AuthCheck(
personalSelected: args,
),
);
} else {
return MaterialPageRoute(
settings: settings,
builder: (_) => const AuthCheck(
personalSelected: true,
personalSelected: args.personalSelected,
firstBoot: args.firstBoot,
),
);
}
return _errorRoute();
case '/notifications':
if (args is NotificationArguments) {
return MaterialPageRoute(
@@ -284,6 +279,7 @@ Route<dynamic> _errorRoute() {
settings: const RouteSettings(name: '/'),
builder: (_) => const AuthCheck(
personalSelected: true,
firstBoot: true,
));
// return MaterialPageRoute(builder: (_) {
// return const Scaffold(

View File

@@ -8,6 +8,7 @@ import Foundation
import device_info_plus
import firebase_core
import geolocator_apple
import local_auth_darwin
import mobile_scanner
import no_screenshot
import printing
@@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
NoScreenshotPlugin.register(with: registry.registrar(forPlugin: "NoScreenshotPlugin"))
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))

View File

@@ -645,6 +645,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
local_auth:
dependency: "direct main"
description:
name: local_auth
sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
local_auth_android:
dependency: transitive
description:
name: local_auth_android
sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92"
url: "https://pub.dev"
source: hosted
version: "1.0.46"
local_auth_darwin:
dependency: transitive
description:
name: local_auth_darwin
sha256: "5c5127061107278ab4cafa1ac51b3b6760282bf1a2abf011270908a429d1634b"
url: "https://pub.dev"
source: hosted
version: "1.4.2"
local_auth_platform_interface:
dependency: transitive
description:
name: local_auth_platform_interface
sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a"
url: "https://pub.dev"
source: hosted
version: "1.0.10"
local_auth_windows:
dependency: transitive
description:
name: local_auth_windows
sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5
url: "https://pub.dev"
source: hosted
version: "1.0.11"
logging:
dependency: transitive
description:

View File

@@ -67,6 +67,7 @@ dependencies:
barcode_widget: ^2.0.4
url_launcher: ^6.3.1
fl_downloader: ^2.0.2
local_auth: ^2.3.0
dev_dependencies:
flutter_test:

View File

@@ -9,6 +9,7 @@
#include <firebase_core/firebase_core_plugin_c_api.h>
#include <fl_downloader/fl_downloader_plugin_c_api.h>
#include <geolocator_windows/geolocator_windows.h>
#include <local_auth_windows/local_auth_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <printing/printing_plugin.h>
#include <syncfusion_pdfviewer_windows/syncfusion_pdfviewer_windows_plugin.h>
@@ -22,6 +23,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlDownloaderPluginCApi"));
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
LocalAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
PrintingPluginRegisterWithRegistrar(

View File

@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
firebase_core
fl_downloader
geolocator_windows
local_auth_windows
permission_handler_windows
printing
syncfusion_pdfviewer_windows

View File

@@ -45,8 +45,8 @@ services:
ports:
- 3567:3567
environment:
REFRESH_TOKEN_VALIDITY: '1440'
ACCESS_TOKEN_VALIDITY: '3600'
REFRESH_TOKEN_VALIDITY: '43200'
ACCESS_TOKEN_VALIDITY: '43200'
PASSWORD_RESET_TOKEN_LIFETIME: '7200000'
MYSQL_USER: ${SQL_USER}
MYSQL_PASSWORD: ${SQL_USER_PW}