flutter_screen_lock icon indicating copy to clipboard operation
flutter_screen_lock copied to clipboard

request an example that locks down an app

Open csells opened this issue 2 years ago • 7 comments

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.

csells avatar Feb 24 '23 04:02 csells

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.

naoki0719 avatar Feb 24 '23 05:02 naoki0719

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

csells avatar Feb 24 '23 05:02 csells

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

naoki0719 avatar Feb 24 '23 05:02 naoki0719

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 avatar Feb 25 '23 18:02 csells

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.

csells avatar Feb 25 '23 18:02 csells

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.

naoki0719 avatar Feb 26 '23 12:02 naoki0719

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.

csells avatar Feb 27 '23 03:02 csells