Setup common lockscreen

This commit is contained in:
vishnukvmd
2025-07-21 14:51:23 +05:30
committed by AmanRajSinghMourya
parent 14e570b676
commit 3e9032588e
14 changed files with 4076 additions and 0 deletions

View File

@@ -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<bool> 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,
),
],
);
}
}

View File

@@ -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<bool> 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<bool> 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<bool> 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<bool> isLocalAuthSupportedOnDevice() async {
try {
return Platform.isLinux
? await FlutterLocalAuthentication().canAuthenticate()
: await LocalAuthentication().isDeviceSupported();
} on MissingPluginException {
return false;
}
}
}

View File

@@ -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<Duration> 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<void> 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<SignedOutEvent>().listen((event) {
removePinAndPassword();
});
}
Future<void> setOfflineModeWarningStatus(bool value) async {
await _preferences.setBool(keyShowOfflineModeWarning, value);
}
bool getOfflineModeWarningStatus() {
return _preferences.getBool(keyShowOfflineModeWarning) ?? true;
}
Future<void> 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<void> setLightMode(bool isLightMode) async {
if (isLightMode != (_preferences.getBool(kIsLightMode) ?? true)) {
await _preferences.setBool(kIsLightMode, isLightMode);
}
}
Future<void> 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<void> setAutoLockTime(Duration duration) async {
await _preferences.setInt(autoLockTime, duration.inMilliseconds);
}
int getAutoLockTime() {
return _preferences.getInt(autoLockTime) ?? 5000;
}
Future<void> setLastInvalidAttemptTime(int time) async {
await _preferences.setInt(lastInvalidAttemptTime, time);
}
int getlastInvalidAttemptTime() {
return _preferences.getInt(lastInvalidAttemptTime) ?? 0;
}
int getInvalidAttemptCount() {
return _preferences.getInt(keyInvalidAttempts) ?? 0;
}
Future<void> setInvalidAttemptCount(int count) async {
await _preferences.setInt(keyInvalidAttempts, count);
}
Future<void> 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<void> 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<Uint8List?> getSalt() async {
final String? salt = await _secureStorage.read(key: saltKey);
if (salt == null) return null;
return base64Decode(salt);
}
Future<String?> getPin() async {
return _secureStorage.read(key: pin);
}
Future<void> 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<String?> getPassword() async {
return _secureStorage.read(key: password);
}
Future<void> removePinAndPassword() async {
await _secureStorage.delete(key: saltKey);
await _secureStorage.delete(key: pin);
await _secureStorage.delete(key: password);
}
Future<bool> isPinSet() async {
return await _secureStorage.containsKey(key: pin);
}
Future<bool> isPasswordSet() async {
return await _secureStorage.containsKey(key: password);
}
Future<bool> 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<void> 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<void> _clearLsDataInKeychainIfFreshInstall() async {
if ((Platform.isIOS || Platform.isMacOS) && !_config.isLoggedIn()) {
await _secureStorage.delete(key: password);
await _secureStorage.delete(key: pin);
await _secureStorage.delete(key: saltKey);
}
}
}

View File

@@ -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<Locale>? supportedLocales;
final List<LocalizationsDelegate<dynamic>> 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<AppLock> createState() => _AppLockState();
}
class _AppLockState extends State<AppLock> with WidgetsBindingObserver {
static final GlobalKey<NavigatorState> _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>[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<void> 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();
}
}

View File

@@ -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,
),
],
),
),
),
),
),
),
);
}
}

View File

@@ -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<LockScreen> createState() => _LockScreenState();
}
class _LockScreenState extends State<LockScreen> 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<double>(
tween: Tween<double>(
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<void> 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<void> _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<void> _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);
}
}
}

View File

@@ -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<LockScreenAutoLock> createState() => _LockScreenAutoLockState();
}
class _LockScreenAutoLockState extends State<LockScreenAutoLock> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
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<AutoLockItems> createState() => _AutoLockItemsState();
}
class _AutoLockItemsState extends State<AutoLockItems> {
final autoLockDurations = LockScreenSettings.instance.autoLockDurations;
List<Widget> 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;
}
}
}

View File

@@ -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<LockScreenConfirmPassword> createState() =>
_LockScreenConfirmPasswordState();
}
class _LockScreenConfirmPasswordState extends State<LockScreenConfirmPassword> {
final _confirmPasswordController = TextEditingController(text: null);
final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance;
final _focusNode = FocusNode();
final _isFormValid = ValueNotifier<bool>(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<void> _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<bool>(
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)),
],
),
),
),
);
}
}

View File

@@ -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<LockScreenConfirmPin> createState() => _LockScreenConfirmPinState();
}
class _LockScreenConfirmPinState extends State<LockScreenConfirmPin> {
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<void> _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<double>(
tween: Tween<double>(
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();
},
),
],
),
);
}
}

View File

@@ -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<LockScreenOptions> createState() => _LockScreenOptionsState();
}
class _LockScreenOptionsState extends State<LockScreenOptions> {
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<void> _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<void> _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<void> _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<void> _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<void> _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<void> _onAutoLock() async {
await routeToPage(
context,
const LockScreenAutoLock(),
).then(
(value) {
setState(() {
autoLockTimeInMilliseconds = _lockScreenSettings.getAutoLockTime();
});
},
);
}
Future<void> _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: <Widget>[
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) {}
}

View File

@@ -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<LockScreenPassword> createState() => _LockScreenPasswordState();
}
class _LockScreenPasswordState extends State<LockScreenPassword> {
final _passwordController = TextEditingController(text: null);
final _focusNode = FocusNode();
final _isFormValid = ValueNotifier<bool>(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<bool>(
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<bool> _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<void> _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();
}
}
}

View File

@@ -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<LockScreenPin> createState() => _LockScreenPinState();
}
class _LockScreenPinState extends State<LockScreenPin> {
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<bool> 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<void> _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<double>(
tween: Tween<double>(
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);
},
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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: