Closed csells closed 1 month ago
How about using StatefulWidget
with WidgetsBindingObserver
as a mixin?
I think you can implement didChangeAppLifecycleState
and call screenLock
depending on the state of the screen.
I think it would be a useful test if your plugin to build such a sample, yes.
I will try to find time to try, but I work full time and don't know when that will be.
I haven't tried it yet, but here's what Bing chat says about how to use local_auth to secure an app the way I'm interested in:
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
final LocalAuthentication auth = LocalAuthentication();
bool _isAuthenticated = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_authenticate(); // authenticate when app starts
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
print(state);
if (state == AppLifecycleState.resumed) {
// authenticate when app resumes
_authenticate();
}
}
Future<void> _authenticate() async {
bool authenticated = false;
try {
authenticated = await auth.authenticate(
localizedReason: 'Please authenticate to continue using this app',
biometricOnly: true,
useErrorDialogs: true,
stickyAuth: true,
);
} on Exception catch (e) {
print(e);
}
if (!mounted) return;
setState(() {
_isAuthenticated = authenticated;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: _isAuthenticated ? Text('Hello World') : Text('Please authenticate'),
),
),
);
}
}```
I assume that's close to how to do it with your plugin.
I asked ChatGPT to update the code to use flutter_screen_lock and this is what it said:
import 'package:flutter/material.dart';
import 'package:flutter_screen_lock/flutter_screen_lock.dart';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
bool _isAuthenticated = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_authenticate(); // authenticate when app starts
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
print(state);
if (state == AppLifecycleState.resumed) {
// authenticate when app resumes
_authenticate();
}
}
Future<void> _authenticate() async {
bool isAuthenticated = false;
try {
isAuthenticated = await showFlutterLockScreen(
context: context,
title: 'Please authenticate to continue using this app',
cancelButton: 'Cancel',
canAuthenticate: true,
canCreatePassword: true,
biometricAuthConfig: BiometricAuthConfig(
android: AndroidAuthConfig(
useBioMetric: true,
androidAuthType: AndroidAuthType.BIOMETRIC_WEAK,
),
iOS: IOSAuthConfig(
useBioMetric: true,
),
),
);
} catch (e) {
print(e);
}
if (!mounted) return;
setState(() {
_isAuthenticated = isAuthenticated;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: _isAuthenticated ? Text('Hello World') : Text('Please authenticate'),
),
),
);
}
}
I have no idea if this is true or not, however.
The result is the same as my idea.
Every time the app resumes, it will call screenLock
and you will be asked for a passcode.
Nothing that ChatGPT suggests above actually works, although as you say, the hint about WidgetsBindingObserver.didChangeAppLifecycleState
is a good one. The following builds on that hint to show what a mobile Flutter app needs to do if it should always be locked:
// ignore_for_file: library_private_types_in_public_api
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_screen_lock/flutter_screen_lock.dart';
import 'package:shared_preferences/shared_preferences.dart';
late final SharedPreferences prefs;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
prefs = await SharedPreferences.getInstance();
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) => const MaterialApp(
home: HomeScreen(),
);
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
bool _isAuthenticated = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// authenticate when app starts
scheduleMicrotask(_authenticate);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.inactive:
// "logout" when app becomes inactive
setState(() => _isAuthenticated = false);
break;
case AppLifecycleState.resumed:
// authenticate when app resumes
_authenticate();
break;
case AppLifecycleState.paused:
case AppLifecycleState.detached:
break;
}
}
Future<void> _authenticate() async {
final passcode = prefs.getString('passcode');
if (passcode == null) {
// let use create passcode
screenLockCreate(
context: context,
canCancel: false,
onConfirmed: _onLockCreate,
);
} else {
// match passcode to user input
screenLock(
context: context,
correctString: passcode,
canCancel: false,
onUnlocked: _onUnlock,
);
}
}
void _onLockCreate(String value) {
unawaited(prefs.setString('passcode', value));
setState(() => _isAuthenticated = true);
Navigator.pop(context);
}
void _onUnlock() {
setState(() => _isAuthenticated = true);
Navigator.pop(context);
}
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: _isAuthenticated
? const Text('Hello World')
: const Text('Please authenticate'),
),
);
}
You need to update the MainActivity.kt
in your Android app to keep your app's screen from showing in the task switcher when it's been paused:
package com.example.total_screen_lock_example
import android.os.Bundle
import android.view.WindowManager
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE)
}
}
I don't know if there's something equivalent to do for an iOS app.
None of this works for a Flutter desktop app, however, since didChangeAppLifecycleState
never seems to be called in that case. I assume it similarly doesn't work on the web, either.
I'd love an example of an app that blocks the user till they login and makes them login again if they switch back to the app. The current example just shows dialogs when buttons are pushed and don't really show how the plugin works in a real-world situation.