[mob][auth] Implemented Lock screen
This commit is contained in:
@@ -13,6 +13,7 @@ import 'package:ente_auth/models/key_attributes.dart';
|
||||
import 'package:ente_auth/models/key_gen_result.dart';
|
||||
import 'package:ente_auth/models/private_key_attributes.dart';
|
||||
import 'package:ente_auth/store/authenticator_db.dart';
|
||||
import 'package:ente_auth/utils/lock_screen_settings.dart';
|
||||
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -469,7 +470,13 @@ class Configuration {
|
||||
await _preferences.setBool(hasOptedForOfflineModeKey, true);
|
||||
}
|
||||
|
||||
bool shouldShowLockScreen() {
|
||||
Future<bool> shouldShowLockScreen() async {
|
||||
final bool isPin = await LockScreenSettings.instance.isPinSet();
|
||||
final bool isPass = await LockScreenSettings.instance.isPasswordSet();
|
||||
return isPin || isPass || shouldShowSystemLockScreen();
|
||||
}
|
||||
|
||||
bool shouldShowSystemLockScreen() {
|
||||
if (_preferences.containsKey(keyShouldShowLockScreen)) {
|
||||
return _preferences.getBool(keyShouldShowLockScreen)!;
|
||||
} else {
|
||||
@@ -477,7 +484,7 @@ class Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setShouldShowLockScreen(bool value) {
|
||||
Future<void> setSystemLockScreen(bool value) {
|
||||
return _preferences.setBool(keyShouldShowLockScreen, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/ui/tools/app_lock.dart';
|
||||
import 'package:ente_auth/ui/tools/lock_screen.dart';
|
||||
import 'package:ente_auth/ui/utils/icon_utils.dart';
|
||||
import 'package:ente_auth/utils/lock_screen_settings.dart';
|
||||
import 'package:ente_auth/utils/platform_util.dart';
|
||||
import 'package:ente_auth/utils/window_protocol_handler.dart';
|
||||
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
|
||||
@@ -114,7 +115,7 @@ Future<void> _runInForeground() async {
|
||||
AppLock(
|
||||
builder: (args) => App(locale: locale),
|
||||
lockScreen: const LockScreen(),
|
||||
enabled: Configuration.instance.shouldShowLockScreen(),
|
||||
enabled: await Configuration.instance.shouldShowLockScreen(),
|
||||
locale: locale,
|
||||
lightTheme: lightThemeData,
|
||||
darkTheme: darkThemeData,
|
||||
@@ -173,6 +174,7 @@ Future<void> _init(bool bool, {String? via}) async {
|
||||
await NotificationService.instance.init();
|
||||
await UpdateService.instance.init();
|
||||
await IconUtils.instance.init();
|
||||
await LockScreenSettings.instance.init();
|
||||
}
|
||||
|
||||
Future<void> _setupPrivacyScreen() async {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/ui/settings/lock_screen/lock_screen_password.dart';
|
||||
import 'package:ente_auth/ui/settings/lock_screen/lock_screen_pin.dart';
|
||||
import 'package:ente_auth/ui/tools/app_lock.dart';
|
||||
import 'package:ente_auth/utils/auth_util.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
@@ -25,7 +27,7 @@ class LocalAuthenticationService {
|
||||
AppLock.of(context)!.setEnabled(false);
|
||||
final result = await requestAuthentication(context, infoMessage);
|
||||
AppLock.of(context)!.setEnabled(
|
||||
Configuration.instance.shouldShowLockScreen(),
|
||||
await Configuration.instance.shouldShowLockScreen(),
|
||||
);
|
||||
if (!result) {
|
||||
showToast(context, infoMessage);
|
||||
@@ -37,6 +39,47 @@ class LocalAuthenticationService {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> requestEnteAuthForLockScreen(
|
||||
BuildContext context,
|
||||
String? savedPin,
|
||||
String? savedPassword, {
|
||||
bool isOnOpeningApp = false,
|
||||
}) async {
|
||||
if (savedPassword != null) {
|
||||
final result = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return LockScreenPassword(
|
||||
isAuthenticating: true,
|
||||
isOnOpeningApp: isOnOpeningApp,
|
||||
authPass: savedPassword,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (result) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (savedPin != null) {
|
||||
final result = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return LockScreenPin(
|
||||
isAuthenticating: true,
|
||||
isOnOpeningApp: isOnOpeningApp,
|
||||
authPin: savedPin,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (result) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> requestLocalAuthForLockScreen(
|
||||
BuildContext context,
|
||||
bool shouldEnableLockScreen,
|
||||
@@ -53,11 +96,11 @@ class LocalAuthenticationService {
|
||||
if (result) {
|
||||
AppLock.of(context)!.setEnabled(shouldEnableLockScreen);
|
||||
await Configuration.instance
|
||||
.setShouldShowLockScreen(shouldEnableLockScreen);
|
||||
.setSystemLockScreen(shouldEnableLockScreen);
|
||||
return true;
|
||||
} else {
|
||||
AppLock.of(context)!
|
||||
.setEnabled(Configuration.instance.shouldShowLockScreen());
|
||||
.setEnabled(await Configuration.instance.shouldShowLockScreen());
|
||||
}
|
||||
} else {
|
||||
// ignore: unawaited_futures
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:ente_auth/ui/components/separators.dart';
|
||||
import 'package:ente_auth/utils/debouncer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class TextInputWidget extends StatefulWidget {
|
||||
final String? label;
|
||||
@@ -58,6 +59,7 @@ class TextInputWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _TextInputWidgetState extends State<TextInputWidget> {
|
||||
final _logger = Logger("TextInputWidget");
|
||||
ExecutionState executionState = ExecutionState.idle;
|
||||
final _textController = TextEditingController();
|
||||
final _debouncer = Debouncer(const Duration(milliseconds: 300));
|
||||
@@ -66,7 +68,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
||||
///This is to pass if the TextInputWidget is in a dialog and an error is
|
||||
///thrown in executing onSubmit by passing it as arg in Navigator.pop()
|
||||
Exception? _exception;
|
||||
|
||||
bool _incorrectPassword = false;
|
||||
@override
|
||||
void initState() {
|
||||
widget.submitNotifier?.addListener(_onSubmit);
|
||||
@@ -138,7 +140,11 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: colorScheme.strokeFaint),
|
||||
borderSide: BorderSide(
|
||||
color: _incorrectPassword
|
||||
? const Color.fromRGBO(245, 42, 42, 1)
|
||||
: colorScheme.strokeFaint,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
@@ -233,6 +239,10 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
||||
executionState = ExecutionState.error;
|
||||
_debouncer.cancelDebounce();
|
||||
_exception = e as Exception;
|
||||
if (e.toString().contains("Incorrect password")) {
|
||||
_logger.warning("Incorrect password");
|
||||
_surfaceWrongPasswordState();
|
||||
}
|
||||
if (!widget.popNavAfterSubmission) {
|
||||
rethrow;
|
||||
}
|
||||
@@ -306,6 +316,20 @@ class _TextInputWidgetState extends State<TextInputWidget> {
|
||||
void _popNavigatorStack(BuildContext context, {Exception? e}) {
|
||||
Navigator.of(context).canPop() ? Navigator.of(context).pop(e) : null;
|
||||
}
|
||||
|
||||
void _surfaceWrongPasswordState() {
|
||||
setState(() {
|
||||
_incorrectPassword = true;
|
||||
HapticFeedback.vibrate();
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_incorrectPassword = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//todo: Add clear and custom icon for suffic icon
|
||||
|
||||
196
auth/lib/ui/settings/lock_screen/custom_pin_keypad.dart
Normal file
196
auth/lib/ui/settings/lock_screen/custom_pin_keypad.dart
Normal file
@@ -0,0 +1,196 @@
|
||||
import "package:ente_auth/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),
|
||||
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 StatelessWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: muteButton
|
||||
? colorScheme.fillFaintPressed
|
||||
: icon == null
|
||||
? colorScheme.backgroundElevated2
|
||||
: null,
|
||||
),
|
||||
child: Center(
|
||||
child: muteButton
|
||||
? const SizedBox.shrink()
|
||||
: icon != null
|
||||
? Container(
|
||||
child: icon,
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
number,
|
||||
style: textTheme.h3,
|
||||
),
|
||||
Text(
|
||||
text,
|
||||
style: textTheme.tinyBold,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import "package:ente_auth/theme/ente_theme.dart";
|
||||
import "package:ente_auth/ui/common/dynamic_fab.dart";
|
||||
import "package:ente_auth/ui/components/buttons/icon_button_widget.dart";
|
||||
import "package:ente_auth/ui/components/text_input_widget.dart";
|
||||
import "package:ente_auth/utils/lock_screen_settings.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.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: "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(
|
||||
backgroundColor: colorTheme.fillFaintPressed,
|
||||
value: 1,
|
||||
strokeWidth: 1.5,
|
||||
),
|
||||
),
|
||||
IconButtonWidget(
|
||||
icon: Icons.lock,
|
||||
iconButtonType: IconButtonType.primary,
|
||||
iconColor: colorTheme.textBase,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"Re-enter Password",
|
||||
style: textTheme.bodyBold,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(12)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: TextInputWidget(
|
||||
hintText: "Confirm Password",
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
206
auth/lib/ui/settings/lock_screen/lock_screen_confirm_pin.dart
Normal file
206
auth/lib/ui/settings/lock_screen/lock_screen_confirm_pin.dart
Normal file
@@ -0,0 +1,206 @@
|
||||
import "package:ente_auth/theme/ente_theme.dart";
|
||||
import "package:ente_auth/ui/settings/lock_screen/custom_pin_keypad.dart";
|
||||
import "package:ente_auth/utils/lock_screen_settings.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.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;
|
||||
|
||||
final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance;
|
||||
final _pinPutDecoration = PinTheme(
|
||||
height: 48,
|
||||
width: 48,
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
);
|
||||
|
||||
@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,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
return orientation == Orientation.portrait
|
||||
? _getBody(colorTheme, textTheme, isPortrait: true)
|
||||
: SingleChildScrollView(
|
||||
child: _getBody(colorTheme, textTheme, isPortrait: false),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody(colorTheme, textTheme, {required bool isPortrait}) {
|
||||
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(
|
||||
"Re-enter PIN",
|
||||
style: textTheme.bodyBold,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(12)),
|
||||
Pinput(
|
||||
length: 4,
|
||||
showCursor: false,
|
||||
useNativeKeyboard: false,
|
||||
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();
|
||||
},
|
||||
),
|
||||
isPortrait
|
||||
? const Spacer()
|
||||
: const Padding(padding: EdgeInsets.all(12)),
|
||||
CustomPinKeypad(controller: _confirmPinController),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
224
auth/lib/ui/settings/lock_screen/lock_screen_options.dart
Normal file
224
auth/lib/ui/settings/lock_screen/lock_screen_options.dart
Normal file
@@ -0,0 +1,224 @@
|
||||
import "package:ente_auth/core/configuration.dart";
|
||||
import "package:ente_auth/theme/ente_theme.dart";
|
||||
import "package:ente_auth/ui/components/captioned_text_widget.dart";
|
||||
import "package:ente_auth/ui/components/divider_widget.dart";
|
||||
import "package:ente_auth/ui/components/menu_item_widget.dart";
|
||||
import "package:ente_auth/ui/components/title_bar_title_widget.dart";
|
||||
import "package:ente_auth/ui/components/title_bar_widget.dart";
|
||||
import "package:ente_auth/ui/components/toggle_switch_widget.dart";
|
||||
import "package:ente_auth/ui/settings/lock_screen/lock_screen_password.dart";
|
||||
import "package:ente_auth/ui/settings/lock_screen/lock_screen_pin.dart";
|
||||
import "package:ente_auth/ui/tools/app_lock.dart";
|
||||
import "package:ente_auth/utils/lock_screen_settings.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class LockScreenOptions extends StatefulWidget {
|
||||
const LockScreenOptions({super.key});
|
||||
|
||||
@override
|
||||
State<LockScreenOptions> createState() => _LockScreenOptionsState();
|
||||
}
|
||||
|
||||
class _LockScreenOptionsState extends State<LockScreenOptions> {
|
||||
final Configuration _configuration = Configuration.instance;
|
||||
final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance;
|
||||
late bool appLock;
|
||||
bool isPinEnabled = false;
|
||||
bool isPasswordEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeSettings();
|
||||
appLock = isPinEnabled ||
|
||||
isPasswordEnabled ||
|
||||
_configuration.shouldShowSystemLockScreen();
|
||||
}
|
||||
|
||||
Future<void> _initializeSettings() async {
|
||||
final bool passwordEnabled = await _lockscreenSetting.isPasswordSet();
|
||||
final bool pinEnabled = await _lockscreenSetting.isPinSet();
|
||||
setState(() {
|
||||
isPasswordEnabled = passwordEnabled;
|
||||
isPinEnabled = pinEnabled;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _deviceLock() async {
|
||||
await _lockscreenSetting.removePinAndPassword();
|
||||
await _initializeSettings();
|
||||
}
|
||||
|
||||
Future<void> _pinLock() async {
|
||||
final bool result = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LockScreenPin();
|
||||
},
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_initializeSettings();
|
||||
if (result) {
|
||||
appLock = isPinEnabled ||
|
||||
isPasswordEnabled ||
|
||||
_configuration.shouldShowSystemLockScreen();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _passwordLock() async {
|
||||
final bool result = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LockScreenPassword();
|
||||
},
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_initializeSettings();
|
||||
if (result) {
|
||||
appLock = isPinEnabled ||
|
||||
isPasswordEnabled ||
|
||||
_configuration.shouldShowSystemLockScreen();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onToggleSwitch() async {
|
||||
AppLock.of(context)!.setEnabled(!appLock);
|
||||
await _configuration.setSystemLockScreen(!appLock);
|
||||
await _lockscreenSetting.removePinAndPassword();
|
||||
setState(() {
|
||||
_initializeSettings();
|
||||
appLock = !appLock;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorTheme = getEnteColorScheme(context);
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
primary: false,
|
||||
slivers: <Widget>[
|
||||
const TitleBarWidget(
|
||||
flexibleSpaceTitle: TitleBarTitleWidget(
|
||||
title: 'App lock',
|
||||
),
|
||||
),
|
||||
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(
|
||||
children: [
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: 'App lock',
|
||||
),
|
||||
alignCaptionedTextToLeft: true,
|
||||
singleBorderRadius: 8,
|
||||
menuItemColor: colorTheme.fillFaint,
|
||||
trailingWidget: ToggleSwitchWidget(
|
||||
value: () => appLock,
|
||||
onChanged: () => _onToggleSwitch(),
|
||||
),
|
||||
),
|
||||
!appLock
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 14,
|
||||
left: 14,
|
||||
right: 12,
|
||||
),
|
||||
child: Text(
|
||||
'Choose between your device\'s default lock screen and a custom lock screen with a PIN or password.',
|
||||
style: textTheme.miniFaint,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
appLock
|
||||
? Column(
|
||||
children: [
|
||||
MenuItemWidget(
|
||||
captionedTextWidget:
|
||||
const CaptionedTextWidget(
|
||||
title: "Device lock",
|
||||
),
|
||||
alignCaptionedTextToLeft: true,
|
||||
isTopBorderRadiusRemoved: false,
|
||||
isBottomBorderRadiusRemoved: true,
|
||||
menuItemColor: colorTheme.fillFaint,
|
||||
trailingIcon:
|
||||
!(isPasswordEnabled || isPinEnabled)
|
||||
? Icons.check
|
||||
: null,
|
||||
trailingIconColor: colorTheme.textBase,
|
||||
onTap: () => _deviceLock(),
|
||||
),
|
||||
DividerWidget(
|
||||
dividerType: DividerType.menuNoIcon,
|
||||
bgColor: colorTheme.fillFaint,
|
||||
),
|
||||
MenuItemWidget(
|
||||
captionedTextWidget:
|
||||
const CaptionedTextWidget(
|
||||
title: "Pin lock",
|
||||
),
|
||||
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:
|
||||
const CaptionedTextWidget(
|
||||
title: "Password",
|
||||
),
|
||||
alignCaptionedTextToLeft: true,
|
||||
isTopBorderRadiusRemoved: true,
|
||||
isBottomBorderRadiusRemoved: false,
|
||||
menuItemColor: colorTheme.fillFaint,
|
||||
trailingIcon:
|
||||
isPasswordEnabled ? Icons.check : null,
|
||||
trailingIconColor: colorTheme.textBase,
|
||||
onTap: () => _passwordLock(),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
231
auth/lib/ui/settings/lock_screen/lock_screen_password.dart
Normal file
231
auth/lib/ui/settings/lock_screen/lock_screen_password.dart
Normal file
@@ -0,0 +1,231 @@
|
||||
import "package:ente_auth/theme/ente_theme.dart";
|
||||
import "package:ente_auth/ui/common/dynamic_fab.dart";
|
||||
import "package:ente_auth/ui/components/buttons/icon_button_widget.dart";
|
||||
import "package:ente_auth/ui/components/text_input_widget.dart";
|
||||
import "package:ente_auth/ui/settings/lock_screen/lock_screen_confirm_password.dart";
|
||||
import "package:ente_auth/ui/settings/lock_screen/lock_screen_options.dart";
|
||||
import "package:ente_auth/utils/lock_screen_settings.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
|
||||
class LockScreenPassword extends StatefulWidget {
|
||||
const LockScreenPassword({
|
||||
super.key,
|
||||
this.isAuthenticating = false,
|
||||
this.isOnOpeningApp = false,
|
||||
this.authPass,
|
||||
});
|
||||
|
||||
//Is false when setting a new password
|
||||
final bool isAuthenticating;
|
||||
final bool isOnOpeningApp;
|
||||
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: "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(
|
||||
backgroundColor: colorTheme.fillFaintPressed,
|
||||
value: 1,
|
||||
strokeWidth: 1.5,
|
||||
),
|
||||
),
|
||||
IconButtonWidget(
|
||||
icon: Icons.lock,
|
||||
iconButtonType: IconButtonType.primary,
|
||||
iconColor: colorTheme.textBase,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.isAuthenticating ? "Enter Password" : "Set new Password",
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.bodyBold,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(12)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: TextInputWidget(
|
||||
hintText: "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({
|
||||
// "password": utf8.encode(inputtedPassword),
|
||||
// "salt": salt,
|
||||
// "opsLimit": Sodium.cryptoPwhashOpslimitInteractive,
|
||||
// "memLimit": Sodium.cryptoPwhashMemlimitInteractive,
|
||||
// });
|
||||
if (widget.authPass == inputtedPassword) {
|
||||
await _lockscreenSetting.setInvalidAttemptCount(0);
|
||||
|
||||
widget.isOnOpeningApp
|
||||
? Navigator.of(context).pop(true)
|
||||
: Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const LockScreenOptions(),
|
||||
),
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
if (widget.isOnOpeningApp) {
|
||||
invalidAttemptsCount++;
|
||||
if (invalidAttemptsCount > 4) {
|
||||
await _lockscreenSetting.setInvalidAttemptCount(invalidAttemptsCount);
|
||||
Navigator.of(context).pop(false);
|
||||
}
|
||||
}
|
||||
|
||||
await HapticFeedback.vibrate();
|
||||
throw Exception("Incorrect password");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _confirmPassword() async {
|
||||
if (widget.isAuthenticating) {
|
||||
await _confirmPasswordAuth(_passwordController.text);
|
||||
return;
|
||||
} else {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => LockScreenConfirmPassword(
|
||||
password: _passwordController.text,
|
||||
),
|
||||
),
|
||||
);
|
||||
_passwordController.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
269
auth/lib/ui/settings/lock_screen/lock_screen_pin.dart
Normal file
269
auth/lib/ui/settings/lock_screen/lock_screen_pin.dart
Normal file
@@ -0,0 +1,269 @@
|
||||
import "package:ente_auth/theme/colors.dart";
|
||||
import "package:ente_auth/theme/ente_theme.dart";
|
||||
import "package:ente_auth/theme/text_style.dart";
|
||||
import "package:ente_auth/ui/settings/lock_screen/custom_pin_keypad.dart";
|
||||
import "package:ente_auth/ui/settings/lock_screen/lock_screen_confirm_pin.dart";
|
||||
import "package:ente_auth/ui/settings/lock_screen/lock_screen_options.dart";
|
||||
import "package:ente_auth/utils/lock_screen_settings.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import 'package:pinput/pinput.dart';
|
||||
|
||||
class LockScreenPin extends StatefulWidget {
|
||||
const LockScreenPin({
|
||||
super.key,
|
||||
this.isAuthenticating = false,
|
||||
this.isOnOpeningApp = false,
|
||||
this.authPin,
|
||||
});
|
||||
|
||||
//Is false when setting a new password
|
||||
final bool isAuthenticating;
|
||||
final bool isOnOpeningApp;
|
||||
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;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
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({
|
||||
// "password": utf8.encode(code),
|
||||
// "salt": salt,
|
||||
// "opsLimit": Sodium.cryptoPwhashOpslimitInteractive,
|
||||
// "memLimit": Sodium.cryptoPwhashMemlimitInteractive,
|
||||
// });
|
||||
// final String hashedPin = base64Encode(hash);
|
||||
if (widget.authPin == inputtedPin) {
|
||||
invalidAttemptsCount = 0;
|
||||
await _lockscreenSetting.setInvalidAttemptCount(0);
|
||||
widget.isOnOpeningApp
|
||||
? 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.isOnOpeningApp) {
|
||||
invalidAttemptsCount++;
|
||||
if (invalidAttemptsCount > 4) {
|
||||
await _lockscreenSetting.setInvalidAttemptCount(invalidAttemptsCount);
|
||||
Navigator.of(context).pop(false);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _confirmPin(String inputtedPin) async {
|
||||
if (widget.isAuthenticating) {
|
||||
await confirmPinAuth(inputtedPin);
|
||||
return;
|
||||
} else {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) =>
|
||||
LockScreenConfirmPin(pin: inputtedPin),
|
||||
),
|
||||
);
|
||||
_pinController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
final _pinPutDecoration = PinTheme(
|
||||
height: 48,
|
||||
width: 48,
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
);
|
||||
|
||||
@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,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
return orientation == Orientation.portrait
|
||||
? _getBody(colorTheme, textTheme, isPortrait: true)
|
||||
: SingleChildScrollView(
|
||||
child: _getBody(colorTheme, textTheme, isPortrait: false),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody(
|
||||
EnteColorScheme colorTheme,
|
||||
EnteTextTheme textTheme, {
|
||||
required bool isPortrait,
|
||||
}) {
|
||||
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.isAuthenticating ? "Enter PIN" : "Set new PIN",
|
||||
style: textTheme.bodyBold,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(12)),
|
||||
Pinput(
|
||||
length: 4,
|
||||
showCursor: false,
|
||||
useNativeKeyboard: false,
|
||||
controller: _pinController,
|
||||
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);
|
||||
},
|
||||
),
|
||||
isPortrait
|
||||
? const Spacer()
|
||||
: const Padding(padding: EdgeInsets.all(12)),
|
||||
CustomPinKeypad(controller: _pinController),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,15 @@ import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/components/menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/components/toggle_switch_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/common_settings.dart';
|
||||
import 'package:ente_auth/ui/settings/lock_screen/lock_screen_options.dart';
|
||||
import 'package:ente_auth/utils/auth_util.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:ente_auth/utils/platform_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class SecuritySectionWidget extends StatefulWidget {
|
||||
@@ -134,25 +137,34 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
||||
}
|
||||
children.addAll([
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: l10n.lockscreen,
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "App lock",
|
||||
),
|
||||
trailingWidget: ToggleSwitchWidget(
|
||||
value: () => _config.shouldShowLockScreen(),
|
||||
onChanged: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthForLockScreen(
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
if (await LocalAuthentication().isDeviceSupported()) {
|
||||
final bool result = await requestAuthentication(
|
||||
context,
|
||||
!_config.shouldShowLockScreen(),
|
||||
context.l10n.authToChangeLockscreenSetting,
|
||||
context.l10n.lockScreenEnablePreSteps,
|
||||
"Please authenticate to change lockscreen setting",
|
||||
);
|
||||
if (hasAuthenticated) {
|
||||
FocusScope.of(context).requestFocus();
|
||||
setState(() {});
|
||||
if (result) {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LockScreenOptions();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
} else {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
"No system lock found",
|
||||
"To enable app lock, please setup device passcode or screen lock in your system settings.",
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
]);
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/ui/common/gradient_button.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/tools/app_lock.dart';
|
||||
import 'package:ente_auth/utils/auth_util.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/lock_screen_settings.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class LockScreen extends StatefulWidget {
|
||||
@@ -20,11 +26,17 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
|
||||
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) {
|
||||
if (isNonMobileIOSDevice()) {
|
||||
@@ -33,37 +45,133 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
|
||||
}
|
||||
_showLockScreen(source: "postFrameInit");
|
||||
});
|
||||
_platformBrightness =
|
||||
SchedulerBinding.instance.platformDispatcher.platformBrightness;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorTheme = getEnteColorScheme(context);
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
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: [
|
||||
Opacity(
|
||||
opacity: 0.2,
|
||||
child: Image.asset('assets/loading_photos_background.png'),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
width: 180,
|
||||
child: GradientButton(
|
||||
text: context.l10n.unlock,
|
||||
iconData: Icons.lock_open_outlined,
|
||||
onTap: () async {
|
||||
// ignore: unawaited_futures
|
||||
_showLockScreen(source: "tapUnlock");
|
||||
},
|
||||
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: 0,
|
||||
end: _getFractionOfTimeElapsed(),
|
||||
),
|
||||
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(
|
||||
"Too many incorrect attempts",
|
||||
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(
|
||||
"Tap to unlock",
|
||||
style: textTheme.small,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -90,10 +198,17 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
|
||||
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
|
||||
Future.delayed(
|
||||
Duration.zero,
|
||||
() => _showLockScreen(source: "lifeCycle"),
|
||||
);
|
||||
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
|
||||
}
|
||||
@@ -115,24 +230,119 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
|
||||
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");
|
||||
await _lockscreenSetting.setInvalidAttemptCount(0);
|
||||
await showErrorDialog(
|
||||
context,
|
||||
"Too many incorrect attempts",
|
||||
"Please login again",
|
||||
isDismissable: false,
|
||||
);
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
final dialog = createProgressDialog(context, "Logging out ...");
|
||||
await dialog.show();
|
||||
await Configuration.instance.logout();
|
||||
await dialog.hide();
|
||||
}
|
||||
|
||||
Future<void> _showLockScreen({String source = ''}) async {
|
||||
final int id = DateTime.now().millisecondsSinceEpoch;
|
||||
_logger.info("Showing lock screen $source $id");
|
||||
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 = await requestAuthentication(
|
||||
context,
|
||||
context.l10n.authToViewSecrets,
|
||||
);
|
||||
_logger.finest("LockScreen Result $result $id");
|
||||
final result = isTimerRunning
|
||||
? false
|
||||
: await requestAuthentication(
|
||||
context,
|
||||
context.l10n.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");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/lifecycle_event_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pinput/pin_put/pin_put.dart';
|
||||
import 'package:pinput/pinput.dart';
|
||||
|
||||
class TwoFactorAuthenticationPage extends StatefulWidget {
|
||||
final String sessionID;
|
||||
@@ -19,9 +19,13 @@ class TwoFactorAuthenticationPage extends StatefulWidget {
|
||||
class _TwoFactorAuthenticationPageState
|
||||
extends State<TwoFactorAuthenticationPage> {
|
||||
final _pinController = TextEditingController();
|
||||
final _pinPutDecoration = BoxDecoration(
|
||||
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
final _pinPutDecoration = PinTheme(
|
||||
height: 45,
|
||||
width: 45,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
);
|
||||
String _code = "";
|
||||
late LifecycleEventHandler _lifecycleEventHandler;
|
||||
@@ -79,9 +83,9 @@ class _TwoFactorAuthenticationPageState
|
||||
const Padding(padding: EdgeInsets.all(32)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(40, 0, 40, 0),
|
||||
child: PinPut(
|
||||
fieldsCount: 6,
|
||||
onSubmit: (String code) {
|
||||
child: Pinput(
|
||||
length: 6,
|
||||
onCompleted: (String code) {
|
||||
_verifyTwoFactorCode(code);
|
||||
},
|
||||
onChanged: (String pin) {
|
||||
@@ -90,21 +94,34 @@ class _TwoFactorAuthenticationPageState
|
||||
});
|
||||
},
|
||||
controller: _pinController,
|
||||
submittedFieldDecoration: _pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
selectedFieldDecoration: _pinPutDecoration,
|
||||
followingFieldDecoration: _pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
border: Border.all(
|
||||
color: const Color.fromRGBO(45, 194, 98, 0.5),
|
||||
submittedPinTheme: _pinPutDecoration.copyWith(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
border: Border.all(
|
||||
color: const Color.fromRGBO(45, 194, 98, 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
inputDecoration: const InputDecoration(
|
||||
focusedBorder: InputBorder.none,
|
||||
border: InputBorder.none,
|
||||
counterText: '',
|
||||
// submittedFieldDecoration: _pinPutDecoration.copyWith(
|
||||
// borderRadius: BorderRadius.circular(20.0),
|
||||
// ),
|
||||
// selectedFieldDecoration: _pinPutDecoration,
|
||||
defaultPinTheme: _pinPutDecoration,
|
||||
followingPinTheme: _pinPutDecoration.copyWith(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
border: Border.all(
|
||||
color: const Color.fromRGBO(45, 194, 98, 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
// followingFieldDecoration: _pinPutDecoration.copyWith(
|
||||
// borderRadius: BorderRadius.circular(5.0),
|
||||
// border: Border.all(
|
||||
// color: const Color.fromRGBO(45, 194, 98, 0.5),
|
||||
// ),
|
||||
// ),
|
||||
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/utils/lock_screen_settings.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_local_authentication/flutter_local_authentication.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
@@ -8,8 +10,24 @@ import 'package:local_auth_android/local_auth_android.dart';
|
||||
import 'package:local_auth_darwin/types/auth_messages_ios.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
Future<bool> requestAuthentication(BuildContext context, String reason) async {
|
||||
Future<bool> requestAuthentication(
|
||||
BuildContext context,
|
||||
String reason, {
|
||||
bool isOpeningApp = 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,
|
||||
isOnOpeningApp: isOpeningApp,
|
||||
);
|
||||
}
|
||||
if (Platform.isMacOS || Platform.isLinux) {
|
||||
return await FlutterLocalAuthentication().authenticate();
|
||||
} else {
|
||||
|
||||
118
auth/lib/utils/lock_screen_settings.dart
Normal file
118
auth/lib/utils/lock_screen_settings.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
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";
|
||||
|
||||
late SharedPreferences _preferences;
|
||||
|
||||
Future<void> init() async {
|
||||
_preferences = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// static Uint8List _generateSalt() {
|
||||
// return Sodium.randombytesBuf(Sodium.cryptoPwhashSaltbytes);
|
||||
// }
|
||||
|
||||
Future<void> setPin(String userPin) async {
|
||||
//await _secureStorage.delete(key: saltKey);
|
||||
await _preferences.setString(pin, userPin);
|
||||
await _preferences.remove(password);
|
||||
// final salt = _generateSalt();
|
||||
// final hash = cryptoPwHash({
|
||||
// "password": utf8.encode(userPin),
|
||||
// "salt": salt,
|
||||
// "opsLimit": Sodium.cryptoPwhashOpslimitInteractive,
|
||||
// "memLimit": Sodium.cryptoPwhashMemlimitInteractive,
|
||||
// });
|
||||
|
||||
// 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 _preferences.getString(pin);
|
||||
// return _secureStorage.read(key: pin);
|
||||
}
|
||||
|
||||
Future<void> setPassword(String pass) async {
|
||||
await _preferences.setString(password, pass);
|
||||
await _preferences.remove(pin);
|
||||
// await _secureStorage.delete(key: saltKey);
|
||||
|
||||
// final salt = _generateSalt();
|
||||
// final hash = cryptoPwHash({
|
||||
// "password": utf8.encode(pass),
|
||||
// "salt": salt,
|
||||
// "opsLimit": Sodium.cryptoPwhashOpslimitInteractive,
|
||||
// "memLimit": Sodium.cryptoPwhashMemlimitInteractive,
|
||||
// });
|
||||
|
||||
// final String saltPassword = base64Encode(salt);
|
||||
// final String hashPassword = base64Encode(hash);
|
||||
|
||||
// await _secureStorage.write(key: saltKey, value: saltPassword);
|
||||
// await _secureStorage.write(key: password, value: hashPassword);
|
||||
// await _secureStorage.delete(key: pin);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Future<String?> getPassword() async {
|
||||
return _preferences.getString(password);
|
||||
// return _secureStorage.read(key: password);
|
||||
}
|
||||
|
||||
Future<void> removePinAndPassword() async {
|
||||
await _preferences.remove(pin);
|
||||
await _preferences.remove(password);
|
||||
// await _secureStorage.delete(key: saltKey);
|
||||
// await _secureStorage.delete(key: pin);
|
||||
// await _secureStorage.delete(key: password);
|
||||
}
|
||||
|
||||
Future<bool> isPinSet() async {
|
||||
return _preferences.containsKey(pin);
|
||||
// return await _secureStorage.containsKey(key: pin);
|
||||
}
|
||||
|
||||
Future<bool> isPasswordSet() async {
|
||||
return _preferences.containsKey(password);
|
||||
// return await _secureStorage.containsKey(key: password);
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ dependencies:
|
||||
fk_user_agent: ^2.1.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_animate: ^4.1.0
|
||||
flutter_bloc: ^8.0.1
|
||||
flutter_context_menu: ^0.1.3
|
||||
flutter_displaymode: ^0.6.0
|
||||
@@ -77,7 +78,7 @@ dependencies:
|
||||
password_strength: ^0.2.0
|
||||
path: ^1.8.3
|
||||
path_provider: ^2.0.11
|
||||
pinput: ^1.2.2
|
||||
pinput: ^2.0.2
|
||||
pointycastle: ^3.7.3
|
||||
privacy_screen: ^0.0.6
|
||||
protobuf: ^3.0.0
|
||||
|
||||
Reference in New Issue
Block a user