Setup common lockscreen
This commit is contained in:
committed by
AmanRajSinghMourya
parent
14e570b676
commit
3e9032588e
65
mobile/packages/lock_screen/lib/auth_util.dart
Normal file
65
mobile/packages/lock_screen/lib/auth_util.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
253
mobile/packages/lock_screen/lib/lock_screen_settings.dart
Normal file
253
mobile/packages/lock_screen/lib/lock_screen_settings.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
212
mobile/packages/lock_screen/lib/ui/app_lock.dart
Normal file
212
mobile/packages/lock_screen/lib/ui/app_lock.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
235
mobile/packages/lock_screen/lib/ui/custom_pin_keypad.dart
Normal file
235
mobile/packages/lock_screen/lib/ui/custom_pin_keypad.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
371
mobile/packages/lock_screen/lib/ui/lock_screen.dart
Normal file
371
mobile/packages/lock_screen/lib/ui/lock_screen.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
143
mobile/packages/lock_screen/lib/ui/lock_screen_auto_lock.dart
Normal file
143
mobile/packages/lock_screen/lib/ui/lock_screen_auto_lock.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
212
mobile/packages/lock_screen/lib/ui/lock_screen_confirm_pin.dart
Normal file
212
mobile/packages/lock_screen/lib/ui/lock_screen_confirm_pin.dart
Normal 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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
399
mobile/packages/lock_screen/lib/ui/lock_screen_options.dart
Normal file
399
mobile/packages/lock_screen/lib/ui/lock_screen_options.dart
Normal 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) {}
|
||||
}
|
||||
250
mobile/packages/lock_screen/lib/ui/lock_screen_password.dart
Normal file
250
mobile/packages/lock_screen/lib/ui/lock_screen_password.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
285
mobile/packages/lock_screen/lib/ui/lock_screen_pin.dart
Normal file
285
mobile/packages/lock_screen/lib/ui/lock_screen_pin.dart
Normal 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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1279
mobile/packages/lock_screen/pubspec.lock
Normal file
1279
mobile/packages/lock_screen/pubspec.lock
Normal file
File diff suppressed because it is too large
Load Diff
42
mobile/packages/lock_screen/pubspec.yaml
Normal file
42
mobile/packages/lock_screen/pubspec.yaml
Normal 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:
|
||||
Reference in New Issue
Block a user