Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

request an example that locks down an app #98

Open
csells opened this issue Feb 24, 2023 · 7 comments
Open

request an example that locks down an app #98

csells opened this issue Feb 24, 2023 · 7 comments

Comments

@csells
Copy link

csells commented Feb 24, 2023

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
Copy link
Owner

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
Copy link
Author

csells commented Feb 24, 2023

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

@naoki0719
Copy link
Owner

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

@csells
Copy link
Author

csells commented Feb 25, 2023

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
Copy link
Author

csells commented Feb 25, 2023

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
Copy link
Owner

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
Copy link
Author

csells commented Feb 27, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants