Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions lib/common/actions/authentication.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
import 'package:restart_app/restart_app.dart';

import '../constants/constants.dart';
import '../extensions/build_context_extension.dart';
import '../preferences/preference_key.dart';
import '../ui/snack_bar_utils.dart';

/// Asks the user to authenticate for the specified [reason] using the device lock method.
Future<bool> authenticate(BuildContext context, {required String reason}) async {
final localAuthentication = LocalAuthentication();

final canAuthenticate = await localAuthentication.isDeviceSupported();

// If the device has no authentication methods available,
// disable the app lock and restart it to remove the lock screen
if (!canAuthenticate) {
await PreferenceKey.lockApp.set(false);

// The Restart package crashes the app if used in debug mode
if (kReleaseMode) {
await Restart.restartApp();
}

return false;
}

bool authenticated;
try {
authenticated = await localAuthentication.authenticate(localizedReason: reason);
} on LocalAuthException catch (exception) {
if (exception.code != LocalAuthExceptionCode.userCanceled) {
logger.w("Authentication failed", exception);
}

authenticated = false;
}

// The authentication failed
if (!authenticated) {
if (!context.mounted) {
return false;
}

SnackBarUtils().show(context, text: context.l.snack_bar_authentication_failed);

return false;
}

return true;
}
28 changes: 28 additions & 0 deletions lib/common/actions/labels/delete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '../../../models/label/label.dart';
import '../../../providers/labels/labels/labels_provider.dart';
import '../../dialogs/confirmation_dialog.dart';
import '../../extensions/build_context_extension.dart';
import '../authentication.dart';
import 'select.dart';

/// Deletes the [label].
Expand All @@ -23,6 +24,19 @@ Future<bool> deleteLabel(BuildContext context, WidgetRef ref, {required Label la
return false;
}

if (!context.mounted) {
return false;
}

// If required, ask for authentication
if (label.requiresAuthentication) {
final authenticated = await authenticate(context, reason: context.l.lock_page_reason_action);

if (!authenticated) {
return false;
}
}

return await ref.read(labelsProvider.notifier).delete([label]);
}

Expand All @@ -42,6 +56,20 @@ Future<bool> deleteLabels(BuildContext context, WidgetRef ref, {required List<La
return false;
}

if (!context.mounted) {
return false;
}

// If required, ask for authentication
final requiresAuthentication = labels.any((label) => label.requiresAuthentication);
if (requiresAuthentication) {
final authenticated = await authenticate(context, reason: context.l.lock_page_reason_action);

if (!authenticated) {
return false;
}
}

final succeeded = await ref.read(labelsProvider.notifier).delete(labels);

if (context.mounted) {
Expand Down
15 changes: 5 additions & 10 deletions lib/common/actions/labels/lock.dart
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:local_auth/local_auth.dart';

import '../../../models/label/label.dart';
import '../../../providers/labels/labels/labels_provider.dart';
import '../../extensions/build_context_extension.dart';
import '../../preferences/preference_key.dart';
import '../authentication.dart';
import 'select.dart';

/// Toggles whether the [labels] are locked.
Future<bool> toggleLockLabels(BuildContext context, WidgetRef ref, {required List<Label> labels}) async {
final lockLabelPreference = PreferenceKey.lockLabel.preferenceOrDefault;
final anyLocked = labels.any((label) => label.locked);

// If the lock label setting is enabled and a label was locked, then ask to authenticate
if (lockLabelPreference && anyLocked) {
final bool authenticated = await LocalAuthentication().authenticate(
localizedReason: context.l.lock_page_reason_action,
);
// If required, ask for authentication
final requiresAuthentication = labels.any((label) => label.requiresAuthentication);
if (requiresAuthentication) {
final authenticated = await authenticate(context, reason: context.l.lock_page_reason_action);

if (!authenticated) {
return false;
Expand Down
28 changes: 8 additions & 20 deletions lib/common/actions/notes/lock.dart
Original file line number Diff line number Diff line change
@@ -1,35 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:local_auth/local_auth.dart';

import '../../../models/note/note.dart';
import '../../../models/note/note_status.dart';
import '../../../providers/notes/notes_provider.dart';
import '../../../providers/notifiers/notifiers.dart';
import '../../extensions/build_context_extension.dart';
import '../../preferences/preference_key.dart';
import '../authentication.dart';
import 'select.dart';

/// Toggles whether the [notes] are locked.
Future<bool> toggleLockNotes(
BuildContext context,
WidgetRef ref, {
required List<Note> notes,
bool requireAuthentication = false,
}) async {
if (requireAuthentication) {
final lockNotePreference = PreferenceKey.lockNote.preferenceOrDefault;
final anyLocked = notes.any((note) => note.locked);
Future<bool> toggleLockNotes(BuildContext context, WidgetRef ref, {required List<Note> notes}) async {
// If required, ask for authentication
final requiresAuthentication = notes.any((note) => note.requiresAuthentication);
if (requiresAuthentication) {
final authenticated = await authenticate(context, reason: context.l.lock_page_reason_action);

// If the lock note setting is enabled and a note was locked, then ask to authenticate
if (lockNotePreference && anyLocked) {
final bool authenticated = await LocalAuthentication().authenticate(
localizedReason: context.l.lock_page_reason_action,
);

if (!authenticated) {
return false;
}
if (!authenticated) {
return false;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ enum AvailableSwipeAction {
return false;
case lock:
case unlock:
await toggleLockNotes(context, ref, notes: [note], requireAuthentication: true);
await toggleLockNotes(context, ref, notes: [note]);
return false;
case archive:
return await archiveNote(context, ref, note: note);
Expand Down
11 changes: 10 additions & 1 deletion lib/models/label/label.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:isar_community/isar.dart';
import 'package:json_annotation/json_annotation.dart';

import '../../common/preferences/preference_key.dart';
import '../note/note.dart';

part 'label.g.dart';
Expand Down Expand Up @@ -46,6 +47,14 @@ class Label extends Equatable implements Comparable<Label> {
@JsonKey(defaultValue: false)
bool locked;

/// Whether the label requires authentication to be accessed.
@ignore
bool get requiresAuthentication {
final lockLabel = PreferenceKey.lockLabel.preferenceOrDefault;

return lockLabel && locked;
}

/// Whether the note is selected.
///
/// Excluded from JSON because it's only needed temporarily during multi-selection.
Expand All @@ -70,7 +79,7 @@ class Label extends Equatable implements Comparable<Label> {
}

/// Default constructor of a label.
Label({required this.name, required this.colorHex}) : visible = true, pinned = false, locked = true;
Label({required this.name, required this.colorHex}) : visible = true, pinned = false, locked = false;

/// Label from [json] data.
factory Label.fromJson(Map<String, dynamic> json) => _$LabelFromJson(json);
Expand Down
12 changes: 12 additions & 0 deletions lib/models/note/note.dart
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,18 @@ sealed class Note implements Comparable<Note> {
@ignore
bool get hasLockedLabel => labels.any((label) => label.locked);

/// Whether the note requires authentication to be accessed.
@ignore
bool get requiresAuthentication {
final lockNote = PreferenceKey.lockNote.preferenceOrDefault;
final lockLabel = PreferenceKey.lockLabel.preferenceOrDefault;

final shouldLockNote = lockNote && locked;
final shouldLockLabel = lockLabel && hasLockedLabel;

return shouldLockNote || shouldLockLabel;
}

/// The [labels] as markdown.
@ignore
String get labelsAsMarkdown => '> ${labelsNamesVisibleSorted.join(', ')}';
Expand Down
8 changes: 1 addition & 7 deletions lib/pages/editor/editor_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,6 @@ class _EditorState extends ConsumerState<EditorPage> {
return const LoadingPlaceholder();
}

final lockNote = PreferenceKey.lockNote.preferenceOrDefault;
final lockLabel = PreferenceKey.lockLabel.preferenceOrDefault;

final showEditorModeButton = PreferenceKey.editorModeButton.preferenceOrDefault;
final focusTitleOnNewNote = PreferenceKey.focusTitleOnNewNote.preferenceOrDefault;
final enableLabels = PreferenceKey.enableLabels.preferenceOrDefault;
Expand Down Expand Up @@ -141,10 +138,7 @@ class _EditorState extends ConsumerState<EditorPage> {
);

// If the note should not be locked, directly return the editor
final shouldLockNote = lockNote && currentNote.locked;
final shouldLockLabel = lockLabel && currentNote.hasLockedLabel;
final shouldLock = shouldLockNote || shouldLockLabel;
if (!shouldLock) {
if (!currentNote.requiresAuthentication) {
return editor;
}

Expand Down
44 changes: 2 additions & 42 deletions lib/pages/lock/lock_page.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import 'package:after_layout/after_layout.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:gap/gap.dart';
import 'package:local_auth/local_auth.dart';
import 'package:restart_app/restart_app.dart';

import '../../common/actions/authentication.dart';
import '../../common/constants/sizes.dart';
import '../../common/extensions/build_context_extension.dart';
import '../../common/preferences/preference_key.dart';
import '../../common/ui/snack_bar_utils.dart';
import '../../common/widgets/asset.dart';
import '../../l10n/app_localizations/app_localizations.g.dart';
import '../../providers/notifiers/lock_notifier.dart';
Expand Down Expand Up @@ -42,15 +37,6 @@ class LockPage extends ConsumerStatefulWidget {
}

class _LockPageState extends ConsumerState<LockPage> with AfterLayoutMixin<LockPage> {
late final LocalAuthentication localAuthentication;

@override
void initState() {
super.initState();

localAuthentication = LocalAuthentication();
}

@override
Future<void> afterFirstLayout(BuildContext context) async {
final l = AppLocalizations.of(context);
Expand All @@ -61,35 +47,9 @@ class _LockPageState extends ConsumerState<LockPage> with AfterLayoutMixin<LockP

/// Asks the user to authenticate to unlock the application.
Future<void> unlock(AppLocalizations l) async {
final canAuthenticate = await localAuthentication.isDeviceSupported();

// If the device has no authentication methods available,
// then disable the app lock and restart it to remove the lock screen
if (!canAuthenticate) {
await PreferenceKey.lockApp.set(false);

// The Restart package crashes the app if used in debug mode
if (kReleaseMode) {
await Restart.restartApp();
}

return;
}
final bool authenticated = await authenticate(context, reason: widget.reason);

final bool authenticated = await localAuthentication.authenticate(localizedReason: widget.reason);

// The authentication failed
if (!authenticated) {
if (!mounted) {
return;
}

SnackBarUtils().show(context, text: context.l.snack_bar_authentication_failed);

return;
}

if (!mounted) {
return;
}

Expand Down
Loading
Loading