From 3e9032588e8c1d482c7d9fba3e8a9b7b87181c5d Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Mon, 21 Jul 2025 14:51:23 +0530 Subject: [PATCH] Setup common lockscreen --- .../packages/lock_screen/lib/auth_util.dart | 65 + .../lib/local_authentication_service.dart | 144 ++ .../lock_screen/lib/lock_screen_settings.dart | 253 ++++ .../packages/lock_screen/lib/ui/app_lock.dart | 212 +++ .../lock_screen/lib/ui/custom_pin_keypad.dart | 235 +++ .../lock_screen/lib/ui/lock_screen.dart | 371 +++++ .../lib/ui/lock_screen_auto_lock.dart | 143 ++ .../lib/ui/lock_screen_confirm_password.dart | 186 +++ .../lib/ui/lock_screen_confirm_pin.dart | 212 +++ .../lib/ui/lock_screen_options.dart | 399 +++++ .../lib/ui/lock_screen_password.dart | 250 ++++ .../lock_screen/lib/ui/lock_screen_pin.dart | 285 ++++ mobile/packages/lock_screen/pubspec.lock | 1279 +++++++++++++++++ mobile/packages/lock_screen/pubspec.yaml | 42 + 14 files changed, 4076 insertions(+) create mode 100644 mobile/packages/lock_screen/lib/auth_util.dart create mode 100644 mobile/packages/lock_screen/lib/local_authentication_service.dart create mode 100644 mobile/packages/lock_screen/lib/lock_screen_settings.dart create mode 100644 mobile/packages/lock_screen/lib/ui/app_lock.dart create mode 100644 mobile/packages/lock_screen/lib/ui/custom_pin_keypad.dart create mode 100644 mobile/packages/lock_screen/lib/ui/lock_screen.dart create mode 100644 mobile/packages/lock_screen/lib/ui/lock_screen_auto_lock.dart create mode 100644 mobile/packages/lock_screen/lib/ui/lock_screen_confirm_password.dart create mode 100644 mobile/packages/lock_screen/lib/ui/lock_screen_confirm_pin.dart create mode 100644 mobile/packages/lock_screen/lib/ui/lock_screen_options.dart create mode 100644 mobile/packages/lock_screen/lib/ui/lock_screen_password.dart create mode 100644 mobile/packages/lock_screen/lib/ui/lock_screen_pin.dart create mode 100644 mobile/packages/lock_screen/pubspec.lock create mode 100644 mobile/packages/lock_screen/pubspec.yaml diff --git a/mobile/packages/lock_screen/lib/auth_util.dart b/mobile/packages/lock_screen/lib/auth_util.dart new file mode 100644 index 0000000000..e1574ab8db --- /dev/null +++ b/mobile/packages/lock_screen/lib/auth_util.dart @@ -0,0 +1,65 @@ +import 'dart:io'; + +import 'package:ente_strings/ente_strings.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_local_authentication/flutter_local_authentication.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:local_auth_android/local_auth_android.dart'; +import 'package:local_auth_darwin/types/auth_messages_ios.dart'; +import 'package:lock_screen/local_authentication_service.dart'; +import 'package:lock_screen/lock_screen_settings.dart'; +import 'package:logging/logging.dart'; + +Future requestAuthentication( + BuildContext context, + String reason, { + bool isOpeningApp = false, + bool isAuthenticatingForInAppChange = false, +}) async { + Logger("AuthUtil").info("Requesting authentication"); + + final String? savedPin = await LockScreenSettings.instance.getPin(); + final String? savedPassword = await LockScreenSettings.instance.getPassword(); + if (savedPassword != null || savedPin != null) { + return await LocalAuthenticationService.instance + .requestEnteAuthForLockScreen( + context, + savedPin, + savedPassword, + isAuthenticatingOnAppLaunch: isOpeningApp, + isAuthenticatingForInAppChange: isAuthenticatingForInAppChange, + ); + } + if (Platform.isMacOS || Platform.isLinux) { + return await FlutterLocalAuthentication().authenticate(); + } else { + await LocalAuthentication().stopAuthentication(); + final l10n = context.strings; + return await LocalAuthentication().authenticate( + localizedReason: reason, + authMessages: [ + AndroidAuthMessages( + biometricHint: l10n.androidBiometricHint, + biometricNotRecognized: l10n.androidBiometricNotRecognized, + biometricRequiredTitle: l10n.androidBiometricRequiredTitle, + biometricSuccess: l10n.androidBiometricSuccess, + cancelButton: l10n.androidCancelButton, + deviceCredentialsRequiredTitle: + l10n.androidDeviceCredentialsRequiredTitle, + deviceCredentialsSetupDescription: + l10n.androidDeviceCredentialsSetupDescription, + goToSettingsButton: l10n.goToSettings, + goToSettingsDescription: l10n.androidGoToSettingsDescription, + signInTitle: l10n.androidSignInTitle, + ), + IOSAuthMessages( + goToSettingsButton: l10n.goToSettings, + goToSettingsDescription: l10n.goToSettings, + lockOut: l10n.iOSLockOut, + // cancelButton default value is "Ok" + cancelButton: l10n.iOSOkButton, + ), + ], + ); + } +} diff --git a/mobile/packages/lock_screen/lib/local_authentication_service.dart b/mobile/packages/lock_screen/lib/local_authentication_service.dart new file mode 100644 index 0000000000..2ff4b0b673 --- /dev/null +++ b/mobile/packages/lock_screen/lib/local_authentication_service.dart @@ -0,0 +1,144 @@ +import 'dart:io'; + +import 'package:ente_ui/utils/dialog_util.dart'; +import 'package:ente_ui/utils/toast_util.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_local_authentication/flutter_local_authentication.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:lock_screen/auth_util.dart'; +import 'package:lock_screen/lock_screen_settings.dart'; +import 'package:lock_screen/ui/app_lock.dart'; +import 'package:lock_screen/ui/lock_screen_password.dart'; +import 'package:lock_screen/ui/lock_screen_pin.dart'; +import 'package:logging/logging.dart'; + +class LocalAuthenticationService { + LocalAuthenticationService._privateConstructor(); + static final LocalAuthenticationService instance = + LocalAuthenticationService._privateConstructor(); + final logger = Logger((LocalAuthenticationService).toString()); + int lastAuthTime = 0; + + Future requestLocalAuthentication( + BuildContext context, + String infoMessage, + ) async { + if (kDebugMode) { + // if last auth time is less than 60 seconds, don't ask for auth again + if (lastAuthTime != 0 && + DateTime.now().millisecondsSinceEpoch - lastAuthTime < 60000) { + return true; + } + } + if (await isLocalAuthSupportedOnDevice() || + LockScreenSettings.instance.getIsAppLockSet()) { + AppLock.of(context)!.setEnabled(false); + final result = await requestAuthentication( + context, + infoMessage, + isAuthenticatingForInAppChange: true, + ); + AppLock.of(context)!.setEnabled( + await LockScreenSettings.instance.shouldShowLockScreen(), + ); + if (!result) { + showToast(context, infoMessage); + return false; + } else { + lastAuthTime = DateTime.now().millisecondsSinceEpoch; + return true; + } + } + return true; + } + + Future requestEnteAuthForLockScreen( + BuildContext context, + String? savedPin, + String? savedPassword, { + bool isAuthenticatingOnAppLaunch = false, + bool isAuthenticatingForInAppChange = false, + }) async { + if (savedPassword != null) { + final result = await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return LockScreenPassword( + isChangingLockScreenSettings: true, + isAuthenticatingForInAppChange: isAuthenticatingForInAppChange, + isAuthenticatingOnAppLaunch: isAuthenticatingOnAppLaunch, + authPass: savedPassword, + ); + }, + ), + ); + if (result) { + return true; + } + } + if (savedPin != null) { + final result = await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return LockScreenPin( + isChangingLockScreenSettings: true, + isAuthenticatingForInAppChange: isAuthenticatingForInAppChange, + isAuthenticatingOnAppLaunch: isAuthenticatingOnAppLaunch, + authPin: savedPin, + ); + }, + ), + ); + if (result) { + return true; + } + } + return false; + } + + Future requestLocalAuthForLockScreen( + BuildContext context, + bool shouldEnableLockScreen, + String infoMessage, + String errorDialogContent, [ + String errorDialogTitle = "", + ]) async { + if (await isLocalAuthSupportedOnDevice()) { + AppLock.of(context)!.disable(); + final result = await requestAuthentication( + context, + infoMessage, + ); + if (result) { + AppLock.of(context)!.setEnabled(shouldEnableLockScreen); + await LockScreenSettings.instance + .setSystemLockScreen(shouldEnableLockScreen); + return true; + } else { + AppLock.of(context)!.setEnabled( + await LockScreenSettings.instance.shouldShowLockScreen(), + ); + } + } else { + // ignore: unawaited_futures + showErrorDialog( + context, + errorDialogTitle, + errorDialogContent, + ); + } + return false; + } + + Future isLocalAuthSupportedOnDevice() async { + try { + return Platform.isLinux + ? await FlutterLocalAuthentication().canAuthenticate() + : await LocalAuthentication().isDeviceSupported(); + } on MissingPluginException { + return false; + } + } +} diff --git a/mobile/packages/lock_screen/lib/lock_screen_settings.dart b/mobile/packages/lock_screen/lib/lock_screen_settings.dart new file mode 100644 index 0000000000..99a41478bf --- /dev/null +++ b/mobile/packages/lock_screen/lib/lock_screen_settings.dart @@ -0,0 +1,253 @@ +import "dart:convert"; +import "dart:io"; +import "dart:typed_data"; + +import "package:ente_configuration/base_configuration.dart"; +import "package:ente_utils/platform_util.dart"; +import "package:ente_events/event_bus.dart"; +import "package:ente_events/models/signed_out_event.dart"; +import "package:ente_crypto_dart/ente_crypto_dart.dart"; +import "package:flutter/material.dart"; +import "package:flutter_secure_storage/flutter_secure_storage.dart"; +import "package:privacy_screen/privacy_screen.dart"; +import "package:shared_preferences/shared_preferences.dart"; + +class LockScreenSettings { + LockScreenSettings._privateConstructor(); + + static final LockScreenSettings instance = + LockScreenSettings._privateConstructor(); + static const password = "ls_password"; + static const pin = "ls_pin"; + static const saltKey = "ls_salt"; + static const keyInvalidAttempts = "ls_invalid_attempts"; + static const lastInvalidAttemptTime = "ls_last_invalid_attempt_time"; + static const autoLockTime = "ls_auto_lock_time"; + static const keyHideAppContent = "ls_hide_app_content"; + static const keyAppLockSet = "ls_is_app_lock_set"; + static const keyHasMigratedLockScreenChanges = + "ls_has_migrated_lock_screen_changes"; + static const keyShowOfflineModeWarning = "ls_show_offline_mode_warning"; + static const keyShouldShowLockScreen = "should_show_lock_screen"; + static const String kIsLightMode = "is_light_mode"; + + final List autoLockDurations = const [ + Duration(milliseconds: 650), + Duration(seconds: 5), + Duration(seconds: 15), + Duration(minutes: 1), + Duration(minutes: 5), + Duration(minutes: 30), + ]; + + late BaseConfiguration _config; + late SharedPreferences _preferences; + late FlutterSecureStorage _secureStorage; + + Future init(BaseConfiguration config) async { + _config = config; + _secureStorage = const FlutterSecureStorage(); + _preferences = await SharedPreferences.getInstance(); + + ///Workaround for privacyScreen not working when app is killed and opened. + await setHideAppContent(getShouldHideAppContent()); + + /// Function to Check if the migration for lock screen changes has + /// already been done by checking a stored boolean value. + await runLockScreenChangesMigration(); + + await _clearLsDataInKeychainIfFreshInstall(); + + Bus.instance.on().listen((event) { + removePinAndPassword(); + }); + } + + Future setOfflineModeWarningStatus(bool value) async { + await _preferences.setBool(keyShowOfflineModeWarning, value); + } + + bool getOfflineModeWarningStatus() { + return _preferences.getBool(keyShowOfflineModeWarning) ?? true; + } + + Future runLockScreenChangesMigration() async { + if (_preferences.getBool(keyHasMigratedLockScreenChanges) != null) { + return; + } + + final bool passwordEnabled = await isPasswordSet(); + final bool pinEnabled = await isPinSet(); + final bool systemLockEnabled = shouldShowSystemLockScreen(); + + if (passwordEnabled || pinEnabled || systemLockEnabled) { + await setAppLockEnabled(true); + } + + await _preferences.setBool(keyHasMigratedLockScreenChanges, true); + } + + Future setLightMode(bool isLightMode) async { + if (isLightMode != (_preferences.getBool(kIsLightMode) ?? true)) { + await _preferences.setBool(kIsLightMode, isLightMode); + } + } + + Future setHideAppContent(bool hideContent) async { + if (PlatformUtil.isDesktop()) return; + final bool isLightMode = _preferences.getBool(kIsLightMode) ?? true; + !hideContent + ? PrivacyScreen.instance.disable() + : await PrivacyScreen.instance.enable( + iosOptions: const PrivacyIosOptions( + enablePrivacy: true, + ), + androidOptions: const PrivacyAndroidOptions( + enableSecure: true, + ), + backgroundColor: + isLightMode ? const Color(0xffffffff) : const Color(0xff000000), + blurEffect: isLightMode + ? PrivacyBlurEffect.extraLight + : PrivacyBlurEffect.extraLight, + ); + await _preferences.setBool(keyHideAppContent, hideContent); + } + + bool getShouldHideAppContent() { + return _preferences.getBool(keyHideAppContent) ?? true; + } + + Future setAutoLockTime(Duration duration) async { + await _preferences.setInt(autoLockTime, duration.inMilliseconds); + } + + int getAutoLockTime() { + return _preferences.getInt(autoLockTime) ?? 5000; + } + + Future setLastInvalidAttemptTime(int time) async { + await _preferences.setInt(lastInvalidAttemptTime, time); + } + + int getlastInvalidAttemptTime() { + return _preferences.getInt(lastInvalidAttemptTime) ?? 0; + } + + int getInvalidAttemptCount() { + return _preferences.getInt(keyInvalidAttempts) ?? 0; + } + + Future setInvalidAttemptCount(int count) async { + await _preferences.setInt(keyInvalidAttempts, count); + } + + Future setAppLockEnabled(bool value) async { + await _preferences.setBool(keyAppLockSet, value); + } + + bool getIsAppLockSet() { + return _preferences.getBool(keyAppLockSet) ?? false; + } + + static Uint8List _generateSalt() { + return sodium.randombytes.buf(sodium.crypto.pwhash.saltBytes); + } + + Future setPin(String userPin) async { + await _secureStorage.delete(key: saltKey); + final salt = _generateSalt(); + + final hash = cryptoPwHash( + utf8.encode(userPin), + salt, + sodium.crypto.pwhash.memLimitInteractive, + sodium.crypto.pwhash.opsLimitSensitive, + sodium, + ); + final String saltPin = base64Encode(salt); + final String hashedPin = base64Encode(hash); + + await _secureStorage.write(key: saltKey, value: saltPin); + await _secureStorage.write(key: pin, value: hashedPin); + await _secureStorage.delete(key: password); + + return; + } + + Future getSalt() async { + final String? salt = await _secureStorage.read(key: saltKey); + if (salt == null) return null; + return base64Decode(salt); + } + + Future getPin() async { + return _secureStorage.read(key: pin); + } + + Future setPassword(String pass) async { + await _secureStorage.delete(key: saltKey); + final salt = _generateSalt(); + + final hash = cryptoPwHash( + utf8.encode(pass), + salt, + sodium.crypto.pwhash.memLimitInteractive, + sodium.crypto.pwhash.opsLimitSensitive, + sodium, + ); + + await _secureStorage.write(key: saltKey, value: base64Encode(salt)); + await _secureStorage.write(key: password, value: base64Encode(hash)); + await _secureStorage.delete(key: pin); + + return; + } + + Future getPassword() async { + return _secureStorage.read(key: password); + } + + Future removePinAndPassword() async { + await _secureStorage.delete(key: saltKey); + await _secureStorage.delete(key: pin); + await _secureStorage.delete(key: password); + } + + Future isPinSet() async { + return await _secureStorage.containsKey(key: pin); + } + + Future isPasswordSet() async { + return await _secureStorage.containsKey(key: password); + } + + Future shouldShowLockScreen() async { + final bool isPin = await isPinSet(); + final bool isPass = await isPasswordSet(); + return isPin || isPass || shouldShowSystemLockScreen(); + } + + bool shouldShowSystemLockScreen() { + if (_preferences.containsKey(keyShouldShowLockScreen)) { + return _preferences.getBool(keyShouldShowLockScreen)!; + } else { + return false; + } + } + + Future setSystemLockScreen(bool value) { + return _preferences.setBool(keyShouldShowLockScreen, value); + } + + // If the app was uninstalled (without logging out if it was used with + // backups), keychain items of the app persist in the keychain. To avoid using + // old keychain items, we delete them on reinstall. + Future _clearLsDataInKeychainIfFreshInstall() async { + if ((Platform.isIOS || Platform.isMacOS) && !_config.isLoggedIn()) { + await _secureStorage.delete(key: password); + await _secureStorage.delete(key: pin); + await _secureStorage.delete(key: saltKey); + } + } +} diff --git a/mobile/packages/lock_screen/lib/ui/app_lock.dart b/mobile/packages/lock_screen/lib/ui/app_lock.dart new file mode 100644 index 0000000000..4e93ce5ec1 --- /dev/null +++ b/mobile/packages/lock_screen/lib/ui/app_lock.dart @@ -0,0 +1,212 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:lock_screen/lock_screen_settings.dart'; + +/// A widget which handles app lifecycle events for showing and hiding a lock screen. +/// This should wrap around a `MyApp` widget (or equivalent). +/// +/// [lockScreen] is a [Widget] which should be a screen for handling login logic and +/// calling `AppLock.of(context).didUnlock();` upon a successful login. +/// +/// [builder] is a [Function] taking an [Object] as its argument and should return a +/// [Widget]. The [Object] argument is provided by the [lockScreen] calling +/// `AppLock.of(context).didUnlock();` with an argument. [Object] can then be injected +/// in to your `MyApp` widget (or equivalent). +/// +/// [enabled] determines wether or not the [lockScreen] should be shown on app launch +/// and subsequent app pauses. This can be changed later on using `AppLock.of(context).enable();`, +/// `AppLock.of(context).disable();` or the convenience method `AppLock.of(context).setEnabled(enabled);` +/// using a bool argument. +/// +/// [backgroundLockLatency] determines how much time is allowed to pass when +/// the app is in the background state before the [lockScreen] widget should be +/// shown upon returning. It defaults to instantly. +/// + +// ignore_for_file: unnecessary_this, library_private_types_in_public_api +class AppLock extends StatefulWidget { + final Widget Function(Object?) builder; + final Widget lockScreen; + final bool enabled; + final Duration backgroundLockLatency; + final ThemeData? darkTheme; + final ThemeData? lightTheme; + final ThemeMode savedThemeMode; + final Locale? locale; + final List? supportedLocales; + final List> localizationsDelegates; + final LocaleListResolutionCallback? localeListResolutionCallback; + + const AppLock({ + super.key, + required this.builder, + required this.lockScreen, + required this.savedThemeMode, + required this.supportedLocales, + required this.localizationsDelegates, + required this.localeListResolutionCallback, + this.enabled = true, + this.locale, + this.backgroundLockLatency = const Duration(seconds: 0), + this.darkTheme, + this.lightTheme, + }); + + static _AppLockState? of(BuildContext context) => + context.findAncestorStateOfType<_AppLockState>(); + + @override + State createState() => _AppLockState(); +} + +class _AppLockState extends State with WidgetsBindingObserver { + static final GlobalKey _navigatorKey = GlobalKey(); + + late bool _didUnlockForAppLaunch; + late bool _isLocked; + late bool _enabled; + + Timer? _backgroundLockLatencyTimer; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addObserver(this); + + this._didUnlockForAppLaunch = !this.widget.enabled; + this._isLocked = false; + this._enabled = this.widget.enabled; + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (!this._enabled) { + return; + } + + if (state == AppLifecycleState.paused && + (!this._isLocked && this._didUnlockForAppLaunch)) { + this._backgroundLockLatencyTimer = Timer( + Duration( + milliseconds: LockScreenSettings.instance.getAutoLockTime(), + ), + () => this.showLockScreen(), + ); + } + + if (state == AppLifecycleState.resumed) { + this._backgroundLockLatencyTimer?.cancel(); + } + + super.didChangeAppLifecycleState(state); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + + this._backgroundLockLatencyTimer?.cancel(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: this.widget.enabled ? this._lockScreen : this.widget.builder(null), + navigatorKey: _navigatorKey, + themeMode: widget.savedThemeMode, + theme: widget.lightTheme, + darkTheme: widget.darkTheme, + locale: widget.locale, + supportedLocales: + widget.supportedLocales ?? const [Locale('en', 'US')], + localeListResolutionCallback: widget.localeListResolutionCallback, + localizationsDelegates: widget.localizationsDelegates, + onGenerateRoute: (settings) { + switch (settings.name) { + case '/lock-screen': + return PageRouteBuilder( + pageBuilder: (_, __, ___) => this._lockScreen, + ); + case '/unlocked': + return PageRouteBuilder( + pageBuilder: (_, __, ___) => + this.widget.builder(settings.arguments), + ); + } + return PageRouteBuilder(pageBuilder: (_, __, ___) => this._lockScreen); + }, + ); + } + + Widget get _lockScreen { + return PopScope( + child: this.widget.lockScreen, + canPop: false, + ); + } + + /// Causes `AppLock` to either pop the [lockScreen] if the app is already running + /// or instantiates widget returned from the [builder] method if the app is cold + /// launched. + /// + /// [args] is an optional argument which will get passed to the [builder] method + /// when built. Use this when you want to inject objects created from the + /// [lockScreen] in to the rest of your app so you can better guarantee that some + /// objects, services or databases are already instantiated before using them. + void didUnlock([Object? args]) { + if (this._didUnlockForAppLaunch) { + this._didUnlockOnAppPaused(); + } else { + this._didUnlockOnAppLaunch(args); + } + } + + /// Makes sure that [AppLock] shows the [lockScreen] on subsequent app pauses if + /// [enabled] is true of makes sure it isn't shown on subsequent app pauses if + /// [enabled] is false. + /// + /// This is a convenience method for calling the [enable] or [disable] method based + /// on [enabled]. + void setEnabled(bool enabled) { + if (enabled) { + this.enable(); + } else { + this.disable(); + } + } + + /// Makes sure that [AppLock] shows the [lockScreen] on subsequent app pauses. + void enable() { + setState(() { + this._enabled = true; + }); + } + + /// Makes sure that [AppLock] doesn't show the [lockScreen] on subsequent app pauses. + void disable() { + setState(() { + this._enabled = false; + }); + } + + /// Manually show the [lockScreen]. + Future showLockScreen() { + this._isLocked = true; + return _navigatorKey.currentState!.pushNamed('/lock-screen'); + } + + void _didUnlockOnAppLaunch(Object? args) { + this._didUnlockForAppLaunch = true; + _navigatorKey.currentState! + .pushReplacementNamed('/unlocked', arguments: args); + } + + void _didUnlockOnAppPaused() { + this._isLocked = false; + _navigatorKey.currentState!.pop(); + } +} diff --git a/mobile/packages/lock_screen/lib/ui/custom_pin_keypad.dart b/mobile/packages/lock_screen/lib/ui/custom_pin_keypad.dart new file mode 100644 index 0000000000..f6fdf9c3e0 --- /dev/null +++ b/mobile/packages/lock_screen/lib/ui/custom_pin_keypad.dart @@ -0,0 +1,235 @@ +import "package:ente_ui/theme/ente_theme.dart"; +import "package:flutter/material.dart"; + +class CustomPinKeypad extends StatelessWidget { + final TextEditingController controller; + const CustomPinKeypad({required this.controller, super.key}); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Container( + padding: const EdgeInsets.all(2), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + color: getEnteColorScheme(context).strokeFainter, + child: Column( + children: [ + Row( + children: [ + _Button( + text: '', + number: '1', + onTap: () { + _onKeyTap('1'); + }, + ), + _Button( + text: "ABC", + number: '2', + onTap: () { + _onKeyTap('2'); + }, + ), + _Button( + text: "DEF", + number: '3', + onTap: () { + _onKeyTap('3'); + }, + ), + ], + ), + Row( + children: [ + _Button( + number: '4', + text: "GHI", + onTap: () { + _onKeyTap('4'); + }, + ), + _Button( + number: '5', + text: 'JKL', + onTap: () { + _onKeyTap('5'); + }, + ), + _Button( + number: '6', + text: 'MNO', + onTap: () { + _onKeyTap('6'); + }, + ), + ], + ), + Row( + children: [ + _Button( + number: '7', + text: 'PQRS', + onTap: () { + _onKeyTap('7'); + }, + ), + _Button( + number: '8', + text: 'TUV', + onTap: () { + _onKeyTap('8'); + }, + ), + _Button( + number: '9', + text: 'WXYZ', + onTap: () { + _onKeyTap('9'); + }, + ), + ], + ), + Row( + children: [ + const _Button( + number: '', + text: '', + muteButton: true, + onTap: null, + ), + _Button( + number: '0', + text: '', + onTap: () { + _onKeyTap('0'); + }, + ), + _Button( + number: '', + text: '', + icon: const Icon(Icons.backspace_outlined), + onTap: () { + _onBackspace(); + }, + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } + + void _onKeyTap(String number) { + controller.text += number; + return; + } + + void _onBackspace() { + if (controller.text.isNotEmpty) { + controller.text = + controller.text.substring(0, controller.text.length - 1); + } + return; + } +} + +class _Button extends StatefulWidget { + final String number; + final String text; + final VoidCallback? onTap; + final bool muteButton; + final Widget? icon; + + const _Button({ + required this.number, + required this.text, + this.muteButton = false, + required this.onTap, + this.icon, + }); + + @override + State<_Button> createState() => _ButtonState(); +} + +class _ButtonState extends State<_Button> { + bool isPressed = false; + + void _onTapDown(TapDownDetails details) { + setState(() { + isPressed = true; + }); + } + + void _onTapUp(TapUpDetails details) async { + setState(() { + isPressed = false; + }); + } + + @override + Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + return Expanded( + child: GestureDetector( + onTap: widget.onTap, + onTapDown: _onTapDown, + onTapUp: _onTapUp, + child: AnimatedContainer( + duration: const Duration(milliseconds: 100), + curve: Curves.easeOut, + child: Container( + margin: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(6), + color: isPressed + ? colorScheme.backgroundElevated + : widget.muteButton + ? colorScheme.fillFaintPressed + : widget.icon == null + ? colorScheme.backgroundElevated2 + : null, + ), + child: Center( + child: widget.muteButton + ? const SizedBox.shrink() + : widget.icon != null + ? Container( + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 10, + ), + child: widget.icon, + ) + : Container( + padding: const EdgeInsets.all(4), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.number, + style: textTheme.h3, + ), + Text( + widget.text, + style: textTheme.tinyBold, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/mobile/packages/lock_screen/lib/ui/lock_screen.dart b/mobile/packages/lock_screen/lib/ui/lock_screen.dart new file mode 100644 index 0000000000..74cbcfc554 --- /dev/null +++ b/mobile/packages/lock_screen/lib/ui/lock_screen.dart @@ -0,0 +1,371 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:ente_accounts/ente_accounts.dart'; +import 'package:ente_configuration/base_configuration.dart'; +import 'package:ente_strings/ente_strings.dart'; +import 'package:ente_ui/theme/ente_theme.dart'; +import 'package:ente_ui/utils/dialog_util.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:lock_screen/auth_util.dart'; +import 'package:lock_screen/lock_screen_settings.dart'; +import 'package:lock_screen/ui/app_lock.dart'; +import 'package:logging/logging.dart'; + +class LockScreen extends StatefulWidget { + final BaseConfiguration config; + + const LockScreen( + this.config, { + super.key, + }); + + @override + State createState() => _LockScreenState(); +} + +class _LockScreenState extends State with WidgetsBindingObserver { + final _logger = Logger("LockScreen"); + bool _isShowingLockScreen = false; + bool _hasPlacedAppInBackground = false; + bool _hasAuthenticationFailed = false; + int? lastAuthenticatingTime; + bool isTimerRunning = false; + int lockedTimeInSeconds = 0; + int invalidAttemptCount = 0; + int remainingTimeInSeconds = 0; + final _lockscreenSetting = LockScreenSettings.instance; + late Brightness _platformBrightness; + + @override + void initState() { + _logger.info("initiatingState"); + super.initState(); + invalidAttemptCount = _lockscreenSetting.getInvalidAttemptCount(); + WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _showLockScreen(source: "postFrameInit"); + }); + _platformBrightness = + SchedulerBinding.instance.platformDispatcher.platformBrightness; + } + + @override + Widget build(BuildContext context) { + final colorTheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + return Scaffold( + appBar: AppBar( + elevation: 0, + leading: widget.config.isLoggedIn() + ? IconButton( + icon: const Icon(Icons.logout_outlined), + color: Theme.of(context).iconTheme.color, + onPressed: () { + _onLogoutTapped(context); + }, + ) + : const SizedBox.shrink(), + ), + body: GestureDetector( + onTap: () { + isTimerRunning ? null : _showLockScreen(source: "tap"); + }, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + opacity: _platformBrightness == Brightness.light ? 0.08 : 0.12, + image: const ExactAssetImage( + 'assets/loading_photos_background.png', + ), + fit: BoxFit.cover, + ), + ), + child: Center( + child: Column( + children: [ + const Spacer(), + SizedBox( + height: 120, + width: 120, + child: Stack( + alignment: Alignment.center, + children: [ + Container( + width: 82, + height: 82, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + Colors.grey.shade500.withOpacity(0.2), + Colors.grey.shade50.withOpacity(0.1), + Colors.grey.shade400.withOpacity(0.2), + Colors.grey.shade300.withOpacity(0.4), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Padding( + padding: const EdgeInsets.all(1.0), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: colorTheme.backgroundBase, + ), + ), + ), + ), + SizedBox( + height: 75, + width: 75, + child: TweenAnimationBuilder( + tween: Tween( + begin: isTimerRunning ? 0 : 1, + end: isTimerRunning + ? _getFractionOfTimeElapsed() + : 1, + ), + duration: const Duration(seconds: 1), + builder: (context, value, _) => + CircularProgressIndicator( + backgroundColor: colorTheme.fillFaintPressed, + value: value, + color: colorTheme.primary400, + strokeWidth: 1.5, + ), + ), + ), + Icon( + Icons.lock, + size: 30, + color: colorTheme.textBase, + ), + ], + ), + ), + const Spacer(), + isTimerRunning + ? Stack( + alignment: Alignment.center, + children: [ + Text( + context.strings.tooManyIncorrectAttempts, + style: textTheme.small, + ) + .animate( + delay: const Duration(milliseconds: 2000), + ) + .fadeOut( + duration: 400.ms, + curve: Curves.easeInOutCirc, + ), + Text( + _formatTime(remainingTimeInSeconds), + style: textTheme.small, + ) + .animate( + delay: const Duration(milliseconds: 2250), + ) + .fadeIn( + duration: 400.ms, + curve: Curves.easeInOutCirc, + ), + ], + ) + : GestureDetector( + onTap: () => _showLockScreen(source: "tap"), + child: Text( + context.strings.tapToUnlock, + style: textTheme.small, + ), + ), + const Padding( + padding: EdgeInsets.only(bottom: 24), + ), + ], + ), + ), + ), + ), + ); + } + + void _onLogoutTapped(BuildContext context) { + showChoiceActionSheet( + context, + title: context.strings.areYouSureYouWantToLogout, + firstButtonLabel: context.strings.yesLogout, + isCritical: true, + firstButtonOnTap: () async { + await UserService.instance.logout(context); + // To start the app afresh, resetting all state. + Process.killPid(pid, ProcessSignal.sigkill); + }, + ); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + _logger.info(state.toString()); + if (state == AppLifecycleState.resumed && !_isShowingLockScreen) { + // This is triggered either when the lock screen is dismissed or when + // the app is brought to foreground + _hasPlacedAppInBackground = false; + final bool didAuthInLast5Seconds = lastAuthenticatingTime != null && + DateTime.now().millisecondsSinceEpoch - lastAuthenticatingTime! < + 5000; + if (!_hasAuthenticationFailed && !didAuthInLast5Seconds) { + // Show the lock screen again only if the app is resuming from the + // background, and not when the lock screen was explicitly dismissed + if (_lockscreenSetting.getlastInvalidAttemptTime() > + DateTime.now().millisecondsSinceEpoch && + !_isShowingLockScreen) { + final int time = (_lockscreenSetting.getlastInvalidAttemptTime() - + DateTime.now().millisecondsSinceEpoch) ~/ + 1000; + Future.delayed(Duration.zero, () { + startLockTimer(time); + _showLockScreen(source: "lifeCycle"); + }); + } + } else { + _hasAuthenticationFailed = false; // Reset failure state + } + } else if (state == AppLifecycleState.paused || + state == AppLifecycleState.inactive) { + // This is triggered either when the lock screen pops up or when + // the app is pushed to background + if (!_isShowingLockScreen) { + _hasPlacedAppInBackground = true; + _hasAuthenticationFailed = false; // reset failure state + } + } + } + + @override + void dispose() { + _logger.info('disposing'); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + Future startLockTimer(int timeInSeconds) async { + if (isTimerRunning) { + return; + } + + setState(() { + isTimerRunning = true; + remainingTimeInSeconds = timeInSeconds; + }); + + while (remainingTimeInSeconds > 0) { + await Future.delayed(const Duration(seconds: 1)); + setState(() { + remainingTimeInSeconds--; + }); + } + + setState(() { + isTimerRunning = false; + }); + } + + double _getFractionOfTimeElapsed() { + final int totalLockedTime = + lockedTimeInSeconds = pow(2, invalidAttemptCount - 5).toInt() * 30; + if (remainingTimeInSeconds == 0) return 1; + + return 1 - remainingTimeInSeconds / totalLockedTime; + } + + String _formatTime(int seconds) { + final int hours = seconds ~/ 3600; + final int minutes = (seconds % 3600) ~/ 60; + final int remainingSeconds = seconds % 60; + + if (hours > 0) { + return "${hours}h ${minutes}m"; + } else if (minutes > 0) { + return "${minutes}m ${remainingSeconds}s"; + } else { + return "${remainingSeconds}s"; + } + } + + Future _autoLogoutOnMaxInvalidAttempts() async { + _logger.info("Auto logout on max invalid attempts"); + Navigator.of(context, rootNavigator: true).pop('dialog'); + Navigator.of(context).popUntil((route) => route.isFirst); + final dialog = createProgressDialog(context, context.strings.loggingOut); + await dialog.show(); + await widget.config.logout(); + await dialog.hide(); + } + + Future _showLockScreen({String source = ''}) async { + final int currentTimestamp = DateTime.now().millisecondsSinceEpoch; + _logger.info("Showing lock screen $source $currentTimestamp"); + try { + if (currentTimestamp < _lockscreenSetting.getlastInvalidAttemptTime() && + !_isShowingLockScreen) { + final int remainingTime = + (_lockscreenSetting.getlastInvalidAttemptTime() - + currentTimestamp) ~/ + 1000; + + await startLockTimer(remainingTime); + } + _isShowingLockScreen = true; + final result = isTimerRunning + ? false + : await requestAuthentication( + context, + context.strings.authToViewSecrets, + isOpeningApp: true, + ); + _logger.finest("LockScreen Result $result $currentTimestamp"); + _isShowingLockScreen = false; + if (result) { + lastAuthenticatingTime = DateTime.now().millisecondsSinceEpoch; + AppLock.of(context)?.didUnlock(); + await _lockscreenSetting.setInvalidAttemptCount(0); + setState(() { + lockedTimeInSeconds = 15; + isTimerRunning = false; + }); + } else { + if (!_hasPlacedAppInBackground) { + // Treat this as a failure only if user did not explicitly + // put the app in background + if (_lockscreenSetting.getInvalidAttemptCount() > 4 && + invalidAttemptCount != + _lockscreenSetting.getInvalidAttemptCount()) { + invalidAttemptCount = _lockscreenSetting.getInvalidAttemptCount(); + + if (invalidAttemptCount > 9) { + await _autoLogoutOnMaxInvalidAttempts(); + return; + } + + lockedTimeInSeconds = pow(2, invalidAttemptCount - 5).toInt() * 30; + await _lockscreenSetting.setLastInvalidAttemptTime( + DateTime.now().millisecondsSinceEpoch + + lockedTimeInSeconds * 1000, + ); + await startLockTimer(lockedTimeInSeconds); + } + _hasAuthenticationFailed = true; + _logger.info("Authentication failed"); + } + } + } catch (e, s) { + _isShowingLockScreen = false; + _logger.severe(e, s); + } + } +} diff --git a/mobile/packages/lock_screen/lib/ui/lock_screen_auto_lock.dart b/mobile/packages/lock_screen/lib/ui/lock_screen_auto_lock.dart new file mode 100644 index 0000000000..dfdc11b8f5 --- /dev/null +++ b/mobile/packages/lock_screen/lib/ui/lock_screen_auto_lock.dart @@ -0,0 +1,143 @@ +import 'package:ente_strings/ente_strings.dart'; +import 'package:ente_ui/components/captioned_text_widget.dart'; +import 'package:ente_ui/components/divider_widget.dart'; +import 'package:ente_ui/components/menu_item_widget.dart'; +import 'package:ente_ui/components/separators.dart'; +import 'package:ente_ui/components/title_bar_title_widget.dart'; +import 'package:ente_ui/components/title_bar_widget.dart'; +import 'package:ente_ui/theme/ente_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:lock_screen/lock_screen_settings.dart'; + +class LockScreenAutoLock extends StatefulWidget { + const LockScreenAutoLock({super.key}); + + @override + State createState() => _LockScreenAutoLockState(); +} + +class _LockScreenAutoLockState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + primary: false, + slivers: [ + TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: context.strings.autoLock, + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return const Padding( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 20, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(8)), + child: AutoLockItems(), + ), + ], + ), + ], + ), + ); + }, + childCount: 1, + ), + ), + ], + ), + ); + } +} + +class AutoLockItems extends StatefulWidget { + const AutoLockItems({super.key}); + + @override + State createState() => _AutoLockItemsState(); +} + +class _AutoLockItemsState extends State { + final autoLockDurations = LockScreenSettings.instance.autoLockDurations; + List items = []; + Duration currentAutoLockTime = const Duration(seconds: 5); + + @override + void initState() { + for (Duration autoLockDuration in autoLockDurations) { + if (autoLockDuration.inMilliseconds == + LockScreenSettings.instance.getAutoLockTime()) { + currentAutoLockTime = autoLockDuration; + break; + } + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + items.clear(); + for (Duration autoLockDuration in autoLockDurations) { + items.add( + _menuItemForPicker(autoLockDuration), + ); + } + items = addSeparators( + items, + DividerWidget( + dividerType: DividerType.menuNoIcon, + bgColor: getEnteColorScheme(context).fillFaint, + ), + ); + return Column( + mainAxisSize: MainAxisSize.min, + children: items, + ); + } + + Widget _menuItemForPicker(Duration autoLockTime) { + return MenuItemWidget( + key: ValueKey(autoLockTime), + menuItemColor: getEnteColorScheme(context).fillFaint, + captionedTextWidget: CaptionedTextWidget( + title: _formatTime(autoLockTime), + ), + trailingIcon: currentAutoLockTime == autoLockTime ? Icons.check : null, + alignCaptionedTextToLeft: true, + isTopBorderRadiusRemoved: true, + isBottomBorderRadiusRemoved: true, + showOnlyLoadingState: true, + onTap: () async { + await LockScreenSettings.instance.setAutoLockTime(autoLockTime).then( + (value) => { + setState(() { + currentAutoLockTime = autoLockTime; + }), + }, + ); + }, + ); + } + + String _formatTime(Duration duration) { + if (duration.inHours != 0) { + return "${duration.inHours}hr"; + } else if (duration.inMinutes != 0) { + return "${duration.inMinutes}m"; + } else if (duration.inSeconds != 0) { + return "${duration.inSeconds}s"; + } else { + return context.strings.immediately; + } + } +} diff --git a/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_password.dart b/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_password.dart new file mode 100644 index 0000000000..e6f8bc0ae2 --- /dev/null +++ b/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_password.dart @@ -0,0 +1,186 @@ +import "package:ente_strings/ente_strings.dart"; +import "package:ente_ui/components/buttons/dynamic_fab.dart"; +import "package:ente_ui/components/buttons/icon_button_widget.dart"; +import "package:ente_ui/components/text_input_widget.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:lock_screen/lock_screen_settings.dart"; + +class LockScreenConfirmPassword extends StatefulWidget { + const LockScreenConfirmPassword({ + super.key, + required this.password, + }); + final String password; + + @override + State createState() => + _LockScreenConfirmPasswordState(); +} + +class _LockScreenConfirmPasswordState extends State { + final _confirmPasswordController = TextEditingController(text: null); + final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance; + final _focusNode = FocusNode(); + final _isFormValid = ValueNotifier(false); + final _submitNotifier = ValueNotifier(false); + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + _focusNode.requestFocus(); + }); + } + + @override + void dispose() { + _submitNotifier.dispose(); + _focusNode.dispose(); + _isFormValid.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + Future _confirmPasswordMatch() async { + if (widget.password == _confirmPasswordController.text) { + await _lockscreenSetting.setPassword(_confirmPasswordController.text); + + Navigator.of(context).pop(true); + Navigator.of(context).pop(true); + return; + } + await HapticFeedback.vibrate(); + throw Exception("Incorrect password"); + } + + @override + Widget build(BuildContext context) { + final colorTheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + final isKeypadOpen = MediaQuery.viewInsetsOf(context).bottom > 100; + + FloatingActionButtonLocation? fabLocation() { + if (isKeypadOpen) { + return null; + } else { + return FloatingActionButtonLocation.centerFloat; + } + } + + return Scaffold( + resizeToAvoidBottomInset: isKeypadOpen, + appBar: AppBar( + elevation: 0, + leading: IconButton( + onPressed: () { + FocusScope.of(context).unfocus(); + Navigator.of(context).pop(); + }, + icon: Icon( + Icons.arrow_back, + color: colorTheme.textBase, + ), + ), + ), + floatingActionButton: ValueListenableBuilder( + valueListenable: _isFormValid, + builder: (context, isFormValid, child) { + return DynamicFAB( + isKeypadOpen: isKeypadOpen, + buttonText: context.strings.confirm, + isFormValid: isFormValid, + onPressedFunction: () async { + _submitNotifier.value = !_submitNotifier.value; + }, + ); + }, + ), + floatingActionButtonLocation: fabLocation(), + floatingActionButtonAnimator: NoScalingAnimation(), + body: SingleChildScrollView( + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: 120, + width: 120, + child: Stack( + alignment: Alignment.center, + children: [ + Container( + width: 82, + height: 82, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + Colors.grey.shade500.withOpacity(0.2), + Colors.grey.shade50.withOpacity(0.1), + Colors.grey.shade400.withOpacity(0.2), + Colors.grey.shade300.withOpacity(0.4), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Padding( + padding: const EdgeInsets.all(1.0), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: colorTheme.backgroundBase, + ), + ), + ), + ), + SizedBox( + height: 75, + width: 75, + child: CircularProgressIndicator( + color: colorTheme.fillFaintPressed, + value: 1, + strokeWidth: 1.5, + ), + ), + IconButtonWidget( + icon: Icons.lock, + iconButtonType: IconButtonType.primary, + iconColor: colorTheme.textBase, + ), + ], + ), + ), + Text( + context.strings.reEnterPassword, + style: textTheme.bodyBold, + ), + const Padding(padding: EdgeInsets.all(12)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextInputWidget( + hintText: context.strings.confirmPassword, + autoFocus: true, + textCapitalization: TextCapitalization.none, + isPasswordInput: true, + shouldSurfaceExecutionStates: false, + onChange: (p0) { + _confirmPasswordController.text = p0; + _isFormValid.value = + _confirmPasswordController.text.isNotEmpty; + }, + onSubmit: (p0) { + return _confirmPasswordMatch(); + }, + submitNotifier: _submitNotifier, + ), + ), + const Padding(padding: EdgeInsets.all(12)), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_pin.dart b/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_pin.dart new file mode 100644 index 0000000000..3f97324092 --- /dev/null +++ b/mobile/packages/lock_screen/lib/ui/lock_screen_confirm_pin.dart @@ -0,0 +1,212 @@ +import "dart:io"; + +import "package:ente_strings/ente_strings.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:lock_screen/lock_screen_settings.dart"; +import "package:lock_screen/ui/custom_pin_keypad.dart"; +import "package:pinput/pinput.dart"; + +class LockScreenConfirmPin extends StatefulWidget { + const LockScreenConfirmPin({super.key, required this.pin}); + final String pin; + @override + State createState() => _LockScreenConfirmPinState(); +} + +class _LockScreenConfirmPinState extends State { + final _confirmPinController = TextEditingController(text: null); + bool isConfirmPinValid = false; + bool isPlatformDesktop = false; + final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance; + + @override + void initState() { + super.initState(); + isPlatformDesktop = + Platform.isLinux || Platform.isMacOS || Platform.isWindows; + } + + @override + void dispose() { + super.dispose(); + _confirmPinController.dispose(); + } + + Future _confirmPinMatch() async { + if (widget.pin == _confirmPinController.text) { + await _lockscreenSetting.setPin(_confirmPinController.text); + + Navigator.of(context).pop(true); + Navigator.of(context).pop(true); + return; + } + setState(() { + isConfirmPinValid = true; + }); + await HapticFeedback.vibrate(); + await Future.delayed(const Duration(milliseconds: 75)); + _confirmPinController.clear(); + setState(() { + isConfirmPinValid = false; + }); + } + + @override + Widget build(BuildContext context) { + final colorTheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + return Scaffold( + appBar: AppBar( + elevation: 0, + leading: IconButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + icon: Icon( + Icons.arrow_back, + color: colorTheme.textBase, + ), + ), + ), + floatingActionButton: isPlatformDesktop + ? null + : CustomPinKeypad(controller: _confirmPinController), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + body: SingleChildScrollView( + child: _getBody(colorTheme, textTheme), + ), + ); + } + + Widget _getBody(colorTheme, textTheme) { + final pinPutDecoration = PinTheme( + height: 48, + width: 48, + padding: const EdgeInsets.only(top: 6.0), + decoration: BoxDecoration( + border: Border.all(color: colorTheme.primary500), + borderRadius: BorderRadius.circular(15.0), + ), + ); + + return Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: 120, + width: 120, + child: Stack( + alignment: Alignment.center, + children: [ + Container( + width: 82, + height: 82, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + Colors.grey.shade500.withOpacity(0.2), + Colors.grey.shade50.withOpacity(0.1), + Colors.grey.shade400.withOpacity(0.2), + Colors.grey.shade300.withOpacity(0.4), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Padding( + padding: const EdgeInsets.all(1.0), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: colorTheme.backgroundBase, + ), + ), + ), + ), + SizedBox( + height: 75, + width: 75, + child: ValueListenableBuilder( + valueListenable: _confirmPinController, + builder: (context, value, child) { + return TweenAnimationBuilder( + tween: Tween( + begin: 0, + end: _confirmPinController.text.length / 4, + ), + curve: Curves.ease, + duration: const Duration(milliseconds: 250), + builder: (context, value, _) => + CircularProgressIndicator( + backgroundColor: colorTheme.fillFaintPressed, + value: value, + color: colorTheme.primary400, + strokeWidth: 1.5, + ), + ); + }, + ), + ), + Icon( + Icons.lock, + color: colorTheme.textBase, + size: 30, + ), + ], + ), + ), + Text( + context.strings.reEnterPin, + style: textTheme.bodyBold, + ), + const Padding(padding: EdgeInsets.all(12)), + Pinput( + length: 4, + showCursor: false, + useNativeKeyboard: isPlatformDesktop, + autofocus: true, + controller: _confirmPinController, + defaultPinTheme: pinPutDecoration, + submittedPinTheme: pinPutDecoration.copyWith( + textStyle: textTheme.h3Bold, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + border: Border.all( + color: colorTheme.fillBase, + ), + ), + ), + followingPinTheme: pinPutDecoration.copyWith( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + border: Border.all( + color: colorTheme.fillMuted, + ), + ), + ), + focusedPinTheme: pinPutDecoration, + errorPinTheme: pinPutDecoration.copyWith( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + border: Border.all( + color: colorTheme.warning400, + ), + ), + ), + errorText: '', + obscureText: true, + obscuringCharacter: '*', + forceErrorState: isConfirmPinValid, + onCompleted: (value) async { + await _confirmPinMatch(); + }, + ), + ], + ), + ); + } +} diff --git a/mobile/packages/lock_screen/lib/ui/lock_screen_options.dart b/mobile/packages/lock_screen/lib/ui/lock_screen_options.dart new file mode 100644 index 0000000000..190e4c971f --- /dev/null +++ b/mobile/packages/lock_screen/lib/ui/lock_screen_options.dart @@ -0,0 +1,399 @@ +import "dart:async"; +import "dart:io"; + +import "package:ente_strings/ente_strings.dart"; +import "package:ente_ui/components/buttons/button_widget.dart"; +import "package:ente_ui/components/buttons/models/button_type.dart"; +import "package:ente_ui/components/captioned_text_widget.dart"; +import "package:ente_ui/components/dialog_widget.dart"; +import "package:ente_ui/components/divider_widget.dart"; +import "package:ente_ui/components/menu_item_widget.dart"; +import "package:ente_ui/components/title_bar_title_widget.dart"; +import "package:ente_ui/components/title_bar_widget.dart"; +import "package:ente_ui/components/toggle_switch_widget.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:ente_utils/platform_util.dart"; +import "package:flutter/material.dart"; +import "package:lock_screen/local_authentication_service.dart"; +import "package:lock_screen/lock_screen_settings.dart"; +import "package:lock_screen/ui/app_lock.dart"; +import "package:lock_screen/ui/lock_screen_auto_lock.dart"; +import "package:lock_screen/ui/lock_screen_password.dart"; +import "package:lock_screen/ui/lock_screen_pin.dart"; + +class LockScreenOptions extends StatefulWidget { + const LockScreenOptions({super.key}); + + @override + State createState() => _LockScreenOptionsState(); +} + +class _LockScreenOptionsState extends State { + final LockScreenSettings _lockScreenSettings = LockScreenSettings.instance; + late bool appLock = false; + bool isPinEnabled = false; + bool isPasswordEnabled = false; + late int autoLockTimeInMilliseconds; + late bool hideAppContent; + late bool isSystemLockEnabled = false; + + @override + void initState() { + super.initState(); + hideAppContent = _lockScreenSettings.getShouldHideAppContent(); + autoLockTimeInMilliseconds = _lockScreenSettings.getAutoLockTime(); + _initializeSettings(); + appLock = _lockScreenSettings.getIsAppLockSet(); + } + + Future _initializeSettings() async { + final bool passwordEnabled = await _lockScreenSettings.isPasswordSet(); + final bool pinEnabled = await _lockScreenSettings.isPinSet(); + final bool shouldHideAppContent = + _lockScreenSettings.getShouldHideAppContent(); + final bool systemLockEnabled = + _lockScreenSettings.shouldShowSystemLockScreen(); + setState(() { + isPasswordEnabled = passwordEnabled; + isPinEnabled = pinEnabled; + hideAppContent = shouldHideAppContent; + isSystemLockEnabled = systemLockEnabled; + }); + } + + Future _deviceLock() async { + if (await LocalAuthenticationService.instance + .isLocalAuthSupportedOnDevice()) { + await _lockScreenSettings.removePinAndPassword(); + await _lockScreenSettings.setSystemLockScreen(!isSystemLockEnabled); + } else { + await showDialogWidget( + context: context, + title: context.strings.noSystemLockFound, + body: context.strings.deviceLockEnablePreSteps, + isDismissible: true, + buttons: [ + ButtonWidget( + buttonType: ButtonType.secondary, + labelText: context.strings.ok, + isInAlert: true, + ), + ], + ); + } + await _initializeSettings(); + } + + Future _pinLock() async { + final bool result = await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return const LockScreenPin(); + }, + ), + ); + + if (result) { + await _lockScreenSettings.setSystemLockScreen(false); + await _lockScreenSettings.setAppLockEnabled(true); + setState(() { + appLock = _lockScreenSettings.getIsAppLockSet(); + }); + } + await _initializeSettings(); + } + + Future _passwordLock() async { + final bool result = await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return const LockScreenPassword(); + }, + ), + ); + if (result) { + await _lockScreenSettings.setSystemLockScreen(false); + setState(() { + appLock = _lockScreenSettings.getIsAppLockSet(); + }); + } + await _initializeSettings(); + } + + Future _onToggleSwitch() async { + AppLock.of(context)!.setEnabled(!appLock); + if (await LocalAuthenticationService.instance + .isLocalAuthSupportedOnDevice()) { + await _lockScreenSettings.setSystemLockScreen(!appLock); + await _lockScreenSettings.setAppLockEnabled(!appLock); + } else { + await _lockScreenSettings.setSystemLockScreen(false); + await _lockScreenSettings.setAppLockEnabled(false); + } + await _lockScreenSettings.removePinAndPassword(); + if (PlatformUtil.isMobile()) { + await _lockScreenSettings.setHideAppContent(!appLock); + setState(() { + hideAppContent = _lockScreenSettings.getShouldHideAppContent(); + }); + } + await _initializeSettings(); + setState(() { + appLock = !appLock; + }); + } + + Future _onAutoLock() async { + await routeToPage( + context, + const LockScreenAutoLock(), + ).then( + (value) { + setState(() { + autoLockTimeInMilliseconds = _lockScreenSettings.getAutoLockTime(); + }); + }, + ); + } + + Future _onHideContent() async { + setState(() { + hideAppContent = !hideAppContent; + }); + await _lockScreenSettings.setHideAppContent(hideAppContent); + } + + String _formatTime(Duration duration) { + if (duration.inHours != 0) { + return "in ${duration.inHours} hour${duration.inHours > 1 ? 's' : ''}"; + } else if (duration.inMinutes != 0) { + return "in ${duration.inMinutes} minute${duration.inMinutes > 1 ? 's' : ''}"; + } else if (duration.inSeconds != 0) { + return "in ${duration.inSeconds} second${duration.inSeconds > 1 ? 's' : ''}"; + } else { + return context.strings.immediately; + } + } + + @override + Widget build(BuildContext context) { + final colorTheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + return Scaffold( + body: CustomScrollView( + primary: false, + slivers: [ + TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: context.strings.appLock, + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.strings.appLock, + ), + alignCaptionedTextToLeft: true, + singleBorderRadius: 8, + menuItemColor: colorTheme.fillFaint, + trailingWidget: ToggleSwitchWidget( + value: () => appLock, + onChanged: () => _onToggleSwitch(), + ), + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 210), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: !appLock + ? Padding( + padding: const EdgeInsets.only( + top: 14, + left: 14, + right: 12, + ), + child: Text( + context.strings.appLockDescription, + style: textTheme.miniFaint, + textAlign: TextAlign.left, + ), + ) + : const SizedBox(), + ), + const Padding( + padding: EdgeInsets.only(top: 24), + ), + ], + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 210), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: appLock + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.strings.deviceLock, + ), + surfaceExecutionStates: false, + alignCaptionedTextToLeft: true, + isTopBorderRadiusRemoved: false, + isBottomBorderRadiusRemoved: true, + menuItemColor: colorTheme.fillFaint, + trailingIcon: isSystemLockEnabled + ? Icons.check + : null, + trailingIconColor: colorTheme.textBase, + onTap: () => _deviceLock(), + ), + DividerWidget( + dividerType: DividerType.menuNoIcon, + bgColor: colorTheme.fillFaint, + ), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.strings.pinLock, + ), + surfaceExecutionStates: false, + alignCaptionedTextToLeft: true, + isTopBorderRadiusRemoved: true, + isBottomBorderRadiusRemoved: true, + menuItemColor: colorTheme.fillFaint, + trailingIcon: + isPinEnabled ? Icons.check : null, + trailingIconColor: colorTheme.textBase, + onTap: () => _pinLock(), + ), + DividerWidget( + dividerType: DividerType.menuNoIcon, + bgColor: colorTheme.fillFaint, + ), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.strings.password, + ), + surfaceExecutionStates: false, + alignCaptionedTextToLeft: true, + isTopBorderRadiusRemoved: true, + isBottomBorderRadiusRemoved: false, + menuItemColor: colorTheme.fillFaint, + trailingIcon: isPasswordEnabled + ? Icons.check + : null, + trailingIconColor: colorTheme.textBase, + onTap: () => _passwordLock(), + ), + const SizedBox( + height: 24, + ), + PlatformUtil.isMobile() + ? MenuItemWidget( + captionedTextWidget: + CaptionedTextWidget( + title: context.strings.autoLock, + subTitle: _formatTime( + Duration( + milliseconds: + autoLockTimeInMilliseconds, + ), + ), + ), + surfaceExecutionStates: false, + alignCaptionedTextToLeft: true, + singleBorderRadius: 8, + menuItemColor: colorTheme.fillFaint, + trailingIconColor: + colorTheme.textBase, + onTap: () => _onAutoLock(), + ) + : const SizedBox.shrink(), + PlatformUtil.isMobile() + ? Padding( + padding: const EdgeInsets.only( + top: 14, + left: 14, + right: 12, + ), + child: Text( + context.strings + .autoLockFeatureDescription, + style: textTheme.miniFaint, + textAlign: TextAlign.left, + ), + ) + : const SizedBox.shrink(), + PlatformUtil.isMobile() + ? Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + MenuItemWidget( + captionedTextWidget: + CaptionedTextWidget( + title: context + .strings.hideContent, + ), + alignCaptionedTextToLeft: true, + singleBorderRadius: 8, + menuItemColor: + colorTheme.fillFaint, + trailingWidget: + ToggleSwitchWidget( + value: () => hideAppContent, + onChanged: () => + _onHideContent(), + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 14, + left: 14, + right: 12, + ), + child: Text( + Platform.isAndroid + ? context.strings + .hideContentDescriptionAndroid + : context.strings + .hideContentDescriptioniOS, + style: textTheme.miniFaint, + textAlign: TextAlign.left, + ), + ), + ], + ) + : const SizedBox.shrink(), + ], + ) + : const SizedBox.shrink(), + ), + ], + ), + ), + ); + }, + childCount: 1, + ), + ), + ], + ), + ); + } + + routeToPage(BuildContext context, LockScreenAutoLock lockScreenAutoLock) {} +} diff --git a/mobile/packages/lock_screen/lib/ui/lock_screen_password.dart b/mobile/packages/lock_screen/lib/ui/lock_screen_password.dart new file mode 100644 index 0000000000..a010a327e6 --- /dev/null +++ b/mobile/packages/lock_screen/lib/ui/lock_screen_password.dart @@ -0,0 +1,250 @@ +import "dart:convert"; + +import "package:ente_strings/ente_strings.dart"; +import "package:ente_ui/components/buttons/dynamic_fab.dart"; +import "package:ente_ui/components/buttons/icon_button_widget.dart"; +import "package:ente_ui/components/text_input_widget.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:ente_crypto_dart/ente_crypto_dart.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:lock_screen/lock_screen_settings.dart"; +import "package:lock_screen/ui/lock_screen_confirm_password.dart"; +import "package:lock_screen/ui/lock_screen_options.dart"; + +/// [isChangingLockScreenSettings] Authentication required for changing lock screen settings. +/// Set to true when the app requires the user to authenticate before allowing +/// changes to the lock screen settings. + +/// [isAuthenticatingOnAppLaunch] Authentication required on app launch. +/// Set to true when the app requires the user to authenticate immediately upon opening. + +/// [isAuthenticatingForInAppChange] Authentication required for in-app changes (e.g., email, password). +/// Set to true when the app requires the to authenticate for sensitive actions like email, password changes. + +class LockScreenPassword extends StatefulWidget { + const LockScreenPassword({ + super.key, + this.isChangingLockScreenSettings = false, + this.isAuthenticatingOnAppLaunch = false, + this.isAuthenticatingForInAppChange = false, + this.authPass, + }); + + final bool isChangingLockScreenSettings; + final bool isAuthenticatingOnAppLaunch; + final bool isAuthenticatingForInAppChange; + final String? authPass; + @override + State createState() => _LockScreenPasswordState(); +} + +class _LockScreenPasswordState extends State { + final _passwordController = TextEditingController(text: null); + final _focusNode = FocusNode(); + final _isFormValid = ValueNotifier(false); + final _submitNotifier = ValueNotifier(false); + int invalidAttemptsCount = 0; + + final _lockscreenSetting = LockScreenSettings.instance; + @override + void initState() { + super.initState(); + invalidAttemptsCount = _lockscreenSetting.getInvalidAttemptCount(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + _focusNode.requestFocus(); + }); + } + + @override + void dispose() { + super.dispose(); + _submitNotifier.dispose(); + _focusNode.dispose(); + _isFormValid.dispose(); + _passwordController.dispose(); + } + + @override + Widget build(BuildContext context) { + final colorTheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + final isKeypadOpen = MediaQuery.viewInsetsOf(context).bottom > 100; + + FloatingActionButtonLocation? fabLocation() { + if (isKeypadOpen) { + return null; + } else { + return FloatingActionButtonLocation.centerFloat; + } + } + + return Scaffold( + resizeToAvoidBottomInset: isKeypadOpen, + appBar: AppBar( + elevation: 0, + leading: IconButton( + onPressed: () { + FocusScope.of(context).unfocus(); + Navigator.of(context).pop(false); + }, + icon: Icon( + Icons.arrow_back, + color: colorTheme.textBase, + ), + ), + ), + floatingActionButton: ValueListenableBuilder( + valueListenable: _isFormValid, + builder: (context, isFormValid, child) { + return DynamicFAB( + isKeypadOpen: isKeypadOpen, + buttonText: context.strings.next, + isFormValid: isFormValid, + onPressedFunction: () async { + _submitNotifier.value = !_submitNotifier.value; + }, + ); + }, + ), + floatingActionButtonLocation: fabLocation(), + floatingActionButtonAnimator: NoScalingAnimation(), + body: SingleChildScrollView( + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: 120, + width: 120, + child: Stack( + alignment: Alignment.center, + children: [ + Container( + width: 82, + height: 82, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + Colors.grey.shade500.withOpacity(0.2), + Colors.grey.shade50.withOpacity(0.1), + Colors.grey.shade400.withOpacity(0.2), + Colors.grey.shade300.withOpacity(0.4), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Padding( + padding: const EdgeInsets.all(1.0), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: colorTheme.backgroundBase, + ), + ), + ), + ), + SizedBox( + height: 75, + width: 75, + child: CircularProgressIndicator( + color: colorTheme.fillFaintPressed, + value: 1, + strokeWidth: 1.5, + ), + ), + IconButtonWidget( + icon: Icons.lock, + iconButtonType: IconButtonType.primary, + iconColor: colorTheme.textBase, + ), + ], + ), + ), + Text( + widget.isChangingLockScreenSettings + ? context.strings.enterPassword + : context.strings.setNewPassword, + textAlign: TextAlign.center, + style: textTheme.bodyBold, + ), + const Padding(padding: EdgeInsets.all(12)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextInputWidget( + hintText: context.strings.password, + autoFocus: true, + textCapitalization: TextCapitalization.none, + isPasswordInput: true, + shouldSurfaceExecutionStates: false, + onChange: (p0) { + _passwordController.text = p0; + _isFormValid.value = _passwordController.text.isNotEmpty; + }, + onSubmit: (p0) { + return _confirmPassword(); + }, + submitNotifier: _submitNotifier, + ), + ), + const Padding(padding: EdgeInsets.all(12)), + ], + ), + ), + ), + ); + } + + Future _confirmPasswordAuth(String inputtedPassword) async { + final Uint8List? salt = await _lockscreenSetting.getSalt(); + final hash = cryptoPwHash( + utf8.encode(inputtedPassword), + salt!, + sodium.crypto.pwhash.memLimitInteractive, + sodium.crypto.pwhash.opsLimitSensitive, + sodium, + ); + if (widget.authPass == base64Encode(hash)) { + await _lockscreenSetting.setInvalidAttemptCount(0); + + widget.isAuthenticatingOnAppLaunch || + widget.isAuthenticatingForInAppChange + ? Navigator.of(context).pop(true) + : Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => const LockScreenOptions(), + ), + ); + return true; + } else { + if (widget.isAuthenticatingOnAppLaunch) { + invalidAttemptsCount++; + await _lockscreenSetting.setInvalidAttemptCount(invalidAttemptsCount); + if (invalidAttemptsCount > 4) { + Navigator.of(context).pop(false); + } + } + + await HapticFeedback.vibrate(); + throw Exception("Incorrect password"); + } + } + + Future _confirmPassword() async { + if (widget.isChangingLockScreenSettings) { + await _confirmPasswordAuth(_passwordController.text); + return; + } else { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => LockScreenConfirmPassword( + password: _passwordController.text, + ), + ), + ); + _passwordController.clear(); + } + } +} diff --git a/mobile/packages/lock_screen/lib/ui/lock_screen_pin.dart b/mobile/packages/lock_screen/lib/ui/lock_screen_pin.dart new file mode 100644 index 0000000000..6a2b6a858b --- /dev/null +++ b/mobile/packages/lock_screen/lib/ui/lock_screen_pin.dart @@ -0,0 +1,285 @@ +import "dart:convert"; +import "dart:io"; + +import "package:ente_strings/ente_strings.dart"; +import "package:ente_ui/theme/colors.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:ente_ui/theme/text_style.dart"; +import "package:ente_crypto_dart/ente_crypto_dart.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:lock_screen/lock_screen_settings.dart"; +import "package:lock_screen/ui/custom_pin_keypad.dart"; +import "package:lock_screen/ui/lock_screen_confirm_pin.dart"; +import "package:lock_screen/ui/lock_screen_options.dart"; +import 'package:pinput/pinput.dart'; + +/// [isChangingLockScreenSettings] Authentication required for changing lock screen settings. +/// Set to true when the app requires the user to authenticate before allowing +/// changes to the lock screen settings. + +/// [isAuthenticatingOnAppLaunch] Authentication required on app launch. +/// Set to true when the app requires the user to authenticate immediately upon opening. + +/// [isAuthenticatingForInAppChange] Authentication required for in-app changes (e.g., email, password). +/// Set to true when the app requires the to authenticate for sensitive actions like email, password changes. + +class LockScreenPin extends StatefulWidget { + const LockScreenPin({ + super.key, + this.isChangingLockScreenSettings = false, + this.isAuthenticatingOnAppLaunch = false, + this.isAuthenticatingForInAppChange = false, + this.authPin, + }); + + final bool isAuthenticatingOnAppLaunch; + final bool isChangingLockScreenSettings; + final bool isAuthenticatingForInAppChange; + final String? authPin; + @override + State createState() => _LockScreenPinState(); +} + +class _LockScreenPinState extends State { + final _pinController = TextEditingController(text: null); + + final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance; + bool isPinValid = false; + int invalidAttemptsCount = 0; + bool isPlatformDesktop = false; + @override + void initState() { + super.initState(); + isPlatformDesktop = + Platform.isLinux || Platform.isMacOS || Platform.isWindows; + invalidAttemptsCount = _lockscreenSetting.getInvalidAttemptCount(); + } + + @override + void dispose() { + super.dispose(); + _pinController.dispose(); + } + + Future confirmPinAuth(String inputtedPin) async { + final Uint8List? salt = await _lockscreenSetting.getSalt(); + final hash = cryptoPwHash( + utf8.encode(inputtedPin), + salt!, + sodium.crypto.pwhash.memLimitInteractive, + sodium.crypto.pwhash.opsLimitSensitive, + sodium, + ); + if (widget.authPin == base64Encode(hash)) { + invalidAttemptsCount = 0; + await _lockscreenSetting.setInvalidAttemptCount(0); + widget.isAuthenticatingOnAppLaunch || + widget.isAuthenticatingForInAppChange + ? Navigator.of(context).pop(true) + : Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => const LockScreenOptions(), + ), + ); + return true; + } else { + setState(() { + isPinValid = true; + }); + await HapticFeedback.vibrate(); + await Future.delayed(const Duration(milliseconds: 75)); + _pinController.clear(); + setState(() { + isPinValid = false; + }); + + if (widget.isAuthenticatingOnAppLaunch) { + invalidAttemptsCount++; + await _lockscreenSetting.setInvalidAttemptCount(invalidAttemptsCount); + if (invalidAttemptsCount > 4) { + Navigator.of(context).pop(false); + } + } + return false; + } + } + + Future _confirmPin(String inputtedPin) async { + if (widget.isChangingLockScreenSettings) { + await confirmPinAuth(inputtedPin); + return; + } else { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => + LockScreenConfirmPin(pin: inputtedPin), + ), + ); + _pinController.clear(); + } + } + + @override + Widget build(BuildContext context) { + final colorTheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + + final pinPutDecoration = PinTheme( + height: 48, + width: 48, + padding: const EdgeInsets.only(top: 6.0), + decoration: BoxDecoration( + border: Border.all(color: colorTheme.primary500), + borderRadius: BorderRadius.circular(15.0), + ), + ); + return Scaffold( + appBar: AppBar( + elevation: 0, + leading: IconButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + icon: Icon( + Icons.arrow_back, + color: colorTheme.textBase, + ), + ), + ), + floatingActionButton: isPlatformDesktop + ? null + : CustomPinKeypad(controller: _pinController), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + body: SingleChildScrollView( + child: _getBody(colorTheme, textTheme, pinPutDecoration), + ), + ); + } + + Widget _getBody( + EnteColorScheme colorTheme, + EnteTextTheme textTheme, + PinTheme pinPutDecoration, + ) { + return Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: 120, + width: 120, + child: Stack( + alignment: Alignment.center, + children: [ + Container( + width: 82, + height: 82, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + Colors.grey.shade500.withOpacity(0.2), + Colors.grey.shade50.withOpacity(0.1), + Colors.grey.shade400.withOpacity(0.2), + Colors.grey.shade300.withOpacity(0.4), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Padding( + padding: const EdgeInsets.all(1.0), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: colorTheme.backgroundBase, + ), + ), + ), + ), + SizedBox( + height: 75, + width: 75, + child: ValueListenableBuilder( + valueListenable: _pinController, + builder: (context, value, child) { + return TweenAnimationBuilder( + tween: Tween( + begin: 0, + end: _pinController.text.length / 4, + ), + curve: Curves.ease, + duration: const Duration(milliseconds: 250), + builder: (context, value, _) => + CircularProgressIndicator( + backgroundColor: colorTheme.fillFaintPressed, + value: value, + color: colorTheme.primary400, + strokeWidth: 1.5, + ), + ); + }, + ), + ), + Icon( + Icons.lock, + color: colorTheme.textBase, + size: 30, + ), + ], + ), + ), + Text( + widget.isChangingLockScreenSettings + ? context.strings.enterPin + : context.strings.setNewPin, + style: textTheme.bodyBold, + ), + const Padding(padding: EdgeInsets.all(12)), + Pinput( + length: 4, + showCursor: false, + useNativeKeyboard: isPlatformDesktop, + controller: _pinController, + autofocus: true, + defaultPinTheme: pinPutDecoration, + submittedPinTheme: pinPutDecoration.copyWith( + textStyle: textTheme.h3Bold, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + border: Border.all( + color: colorTheme.fillBase, + ), + ), + ), + followingPinTheme: pinPutDecoration.copyWith( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + border: Border.all( + color: colorTheme.fillMuted, + ), + ), + ), + focusedPinTheme: pinPutDecoration, + errorPinTheme: pinPutDecoration.copyWith( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + border: Border.all( + color: colorTheme.warning400, + ), + ), + ), + forceErrorState: isPinValid, + obscureText: true, + obscuringCharacter: '*', + errorText: '', + onCompleted: (value) async { + await _confirmPin(_pinController.text); + }, + ), + ], + ), + ); + } +} diff --git a/mobile/packages/lock_screen/pubspec.lock b/mobile/packages/lock_screen/pubspec.lock new file mode 100644 index 0000000000..29bd04fca3 --- /dev/null +++ b/mobile/packages/lock_screen/pubspec.lock @@ -0,0 +1,1279 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + app_links: + dependency: transitive + description: + name: app_links + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" + url: "https://pub.dev" + source: hosted + version: "6.4.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + bip39: + dependency: transitive + description: + name: bip39 + sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc + url: "https://pub.dev" + source: hosted + version: "1.0.6" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cronet_http: + dependency: transitive + description: + name: cronet_http + sha256: df26af0de7c4eff46c53c190b5590e22457bfce6ea679aedb1e6326197f27d6f + url: "https://pub.dev" + source: hosted + version: "1.4.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cupertino_http: + dependency: transitive + description: + name: cupertino_http + sha256: "8fb9e2c36d0732d9d96abd76683406b57e78a2514e27c962e0c603dbe6f2e3f8" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" + url: "https://pub.dev" + source: hosted + version: "9.1.2" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" + dio: + dependency: transitive + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + dotted_border: + dependency: transitive + description: + name: dotted_border + sha256: "99b091ec6891ba0c5331fdc2b502993c7c108f898995739a73c6845d71dad70c" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + email_validator: + dependency: transitive + description: + name: email_validator + sha256: b19aa5d92fdd76fbc65112060c94d45ba855105a28bb6e462de7ff03b12fa1fb + url: "https://pub.dev" + source: hosted + version: "3.0.0" + ente_accounts: + dependency: "direct main" + description: + path: "../accounts" + relative: true + source: path + version: "1.0.0" + ente_base: + dependency: transitive + description: + path: "../base" + relative: true + source: path + version: "1.0.0" + ente_configuration: + dependency: "direct main" + description: + path: "../configuration" + relative: true + source: path + version: "1.0.0" + ente_crypto_dart: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: f91e1545f8263df127762240c4da54a0c42835b2 + url: "https://github.com/ente-io/ente_crypto_dart.git" + source: git + version: "1.0.0" + ente_events: + dependency: "direct main" + description: + path: "../events" + relative: true + source: path + version: "1.0.0" + ente_logging: + dependency: transitive + description: + path: "../logging" + relative: true + source: path + version: "1.0.0" + ente_network: + dependency: transitive + description: + path: "../network" + relative: true + source: path + version: "1.0.0" + ente_strings: + dependency: "direct main" + description: + path: "../strings" + relative: true + source: path + version: "1.0.0" + ente_ui: + dependency: "direct main" + description: + path: "../ui" + relative: true + source: path + version: "1.0.0" + ente_utils: + dependency: "direct main" + description: + path: "../utils" + relative: true + source: path + version: "1.0.0" + event_bus: + dependency: transitive + description: + name: event_bus + sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_saver: + dependency: transitive + description: + name: file_saver + sha256: "9d93db09bd4da9e43238f9dd485360fc51a5c138eea5ef5f407ec56e58079ac0" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" + flutter_email_sender: + dependency: transitive + description: + name: flutter_email_sender + sha256: d39eb5e91358fc19ec4050da69accec21f9d5b2b6bcf188aa246327b6ca2352c + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter_inappwebview: + dependency: transitive + description: + name: flutter_inappwebview + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 + url: "https://pub.dev" + source: hosted + version: "1.3.0+1" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_local_authentication: + dependency: "direct main" + description: + name: flutter_local_authentication + sha256: eb2e471ba77fbc42b6ce8b358939dc9878dc71fcce5a482a8ddcb045563d87cd + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e + url: "https://pub.dev" + source: hosted + version: "2.0.28" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: transitive + description: + name: fluttertoast + sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" + url: "https://pub.dev" + source: hosted + version: "8.2.12" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" + hex: + dependency: transitive + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: transitive + description: + name: http + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + http_profile: + dependency: transitive + description: + name: http_profile + sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + jni: + dependency: transitive + description: + name: jni + sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1 + url: "https://pub.dev" + source: hosted + version: "0.14.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.dev" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.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: "82b2bdeee2199a510d3b7716121e96a6609da86693bb0863edd8566355406b79" + url: "https://pub.dev" + source: hosted + version: "1.0.50" + local_auth_darwin: + dependency: transitive + description: + name: local_auth_darwin + sha256: "25163ce60a5a6c468cf7a0e3dc8a165f824cabc2aa9e39a5e9fc5c2311b7686f" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + 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: "direct main" + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + modal_bottom_sheet: + dependency: transitive + description: + name: modal_bottom_sheet + sha256: eac66ef8cb0461bf069a38c5eb0fa728cee525a531a8304bd3f7b2185407c67e + url: "https://pub.dev" + source: hosted + version: "3.0.0" + native_dio_adapter: + dependency: transitive + description: + name: native_dio_adapter + sha256: "7420bc9517b2abe09810199a19924617b45690a44ecfb0616ac9babc11875c03" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "9f034ba1eeca53ddb339bc8f4813cb07336a849cd735559b60cdc068ecce2dc7" + url: "https://pub.dev" + source: hosted + version: "7.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" + url: "https://pub.dev" + source: hosted + version: "8.3.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + password_strength: + dependency: transitive + description: + name: password_strength + sha256: "0e51e3d864e37873a1347e658147f88b66e141ee36c58e19828dc5637961e1ce" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + pinput: + dependency: "direct main" + description: + name: pinput + sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + privacy_screen: + dependency: "direct main" + description: + name: privacy_screen + sha256: "2856e3a3ed082061a5cd2a1518f1ce6367c55916fb75e5db72e5983033a1ca54" + url: "https://pub.dev" + source: hosted + version: "0.0.8" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + sentry: + dependency: transitive + description: + name: sentry + sha256: "599701ca0693a74da361bc780b0752e1abc98226cf5095f6b069648116c896bb" + url: "https://pub.dev" + source: hosted + version: "8.14.2" + sentry_flutter: + dependency: transitive + description: + name: sentry_flutter + sha256: "5ba2cf40646a77d113b37a07bd69f61bb3ec8a73cbabe5537b05a7c89d2656f8" + url: "https://pub.dev" + source: hosted + version: "8.14.2" + share_plus: + dependency: transitive + description: + name: share_plus + sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0 + url: "https://pub.dev" + source: hosted + version: "11.0.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + sodium: + dependency: transitive + description: + name: sodium + sha256: d9830a388e37c82891888e64cfd4c6764fa3ac716bed80ac6eab89ee42c3cd76 + url: "https://pub.dev" + source: hosted + version: "2.3.1+1" + sodium_libs: + dependency: transitive + description: + name: sodium_libs + sha256: aa764acd6ccc6113e119c2d99471aeeb4637a9a501639549b297d3a143ff49b3 + url: "https://pub.dev" + source: hosted + version: "2.2.1+6" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + step_progress_indicator: + dependency: transitive + description: + name: step_progress_indicator + sha256: b51bb1fcfc78454359f0658c5a2c21548c3825ebf76e826308e9ca10f383bbb8 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + styled_text: + dependency: transitive + description: + name: styled_text + sha256: fd624172cf629751b4f171dd0ecf9acf02a06df3f8a81bb56c0caa4f1df706c3 + url: "https://pub.dev" + source: hosted + version: "8.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + ua_client_hints: + dependency: transitive + description: + name: ua_client_hints + sha256: "1b8759a46bfeab355252881df27f2604c01bded86aa2b578869fb1b638b23118" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + url: "https://pub.dev" + source: hosted + version: "5.14.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + url: "https://pub.dev" + source: hosted + version: "1.1.5" + window_manager: + dependency: transitive + description: + name: window_manager + sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xmlstream: + dependency: transitive + description: + name: xmlstream + sha256: cfc14e3f256997897df9481ae630d94c2d85ada5187ebeb868bb1aabc2c977b4 + url: "https://pub.dev" + source: hosted + version: "1.1.1" +sdks: + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/mobile/packages/lock_screen/pubspec.yaml b/mobile/packages/lock_screen/pubspec.yaml new file mode 100644 index 0000000000..cd611a61f4 --- /dev/null +++ b/mobile/packages/lock_screen/pubspec.yaml @@ -0,0 +1,42 @@ +name: lock_screen +description: A Flutter package containing lock screen UI components and services for Ente apps +version: 1.0.0 +publish_to: none + +environment: + sdk: ">=3.0.0 <4.0.0" + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + ente_accounts: + path: ../accounts + ente_configuration: + path: ../configuration + ente_crypto_dart: + git: + url: https://github.com/ente-io/ente_crypto_dart.git + ente_events: + path: ../events + ente_strings: + path: ../strings + ente_ui: + path: ../ui + ente_utils: + path: ../utils + flutter_animate: ^4.1.0 + flutter_local_authentication: ^1.1.11 + flutter_secure_storage: ^9.0.0 + local_auth: ^2.1.8 + logging: ^1.1.1 + pinput: ^5.0.0 + privacy_screen: ^0.0.8 + shared_preferences: ^2.5.3 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: