naoki0719 / flutter_screen_lock

Provides the ability to lock the screen on ios and android. Biometric authentication can be used in addition to passcode.
https://pub.dev/packages/flutter_screen_lock
MIT License
100 stars 48 forks source link

request an example that locks down an app #98

Closed csells closed 1 month ago

csells commented 1 year ago

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.

naoki0719 commented 1 year 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.

csells commented 1 year ago

I think it would be a useful test if your plugin to build such a sample, yes.

naoki0719 commented 1 year ago

I will try to find time to try, but I work full time and don't know when that will be.

csells commented 1 year ago

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.
csells commented 1 year ago

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.

naoki0719 commented 1 year ago

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.

csells commented 1 year ago

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.