Skip to content

Commit

Permalink
Close #285 screen lock (#299)
Browse files Browse the repository at this point in the history
* Prepare model & setting UI

* Add functionality & UI including PIN/Forget

* Draft save security question

* Initial on main to avoid UI initial glitch

* Refactor setting pin code ; make it smooth

* Add basic analytics

* Required PIN when go to app lock page

* Use old data as initial
  • Loading branch information
theachoem authored Mar 2, 2025
1 parent db5dad9 commit 6a3f899
Show file tree
Hide file tree
Showing 52 changed files with 1,732 additions and 183 deletions.
24 changes: 24 additions & 0 deletions bin/localization/data.csv

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:flutter_quill/flutter_quill.dart';
import 'package:storypad/app_theme.dart';
import 'package:storypad/core/constants/locale_constants.dart';
import 'package:storypad/views/home/home_view.dart';
import 'package:storypad/widgets/sp_local_auth_wrapper.dart';

class App extends StatelessWidget {
const App({
Expand Down Expand Up @@ -34,9 +33,6 @@ class App extends StatelessWidget {
],
supportedLocales: context.supportedLocales,
locale: context.locale,
builder: (context, child) {
return SpLocalAuthWrapper(child: child!);
},
);
}),
);
Expand Down
30 changes: 30 additions & 0 deletions lib/core/objects/app_lock_object.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:storypad/core/types/app_lock_question.dart' show AppLockQuestion;

part 'app_lock_object.g.dart';

@CopyWith()
@JsonSerializable()
class AppLockObject {
final String? pin;
final bool? enabledBiometric;
final Map<AppLockQuestion, String>? securityAnswers;

AppLockObject({
required this.pin,
required this.enabledBiometric,
required this.securityAnswers,
});

factory AppLockObject.init() {
return AppLockObject(
pin: null,
enabledBiometric: false,
securityAnswers: null,
);
}

Map<String, dynamic> toJson() => _$AppLockObjectToJson(this);
factory AppLockObject.fromJson(Map<String, dynamic> json) => _$AppLockObjectFromJson(json);
}
113 changes: 113 additions & 0 deletions lib/core/objects/app_lock_object.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions lib/core/services/analytics/analytics_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,26 @@ class AnalyticsService extends BaseAnalyticsService {
);
}

Future<void> logClearPIN() {
final parameters = sanitizeParameters({});
debug('logClearPIN', parameters);

return FirebaseAnalytics.instance.logEvent(
name: sanitizeEventName('clear_pin'),
parameters: parameters,
);
}

Future<void> logSetPIN() {
final parameters = sanitizeParameters({});
debug('logSetPIN', parameters);

return FirebaseAnalytics.instance.logEvent(
name: sanitizeEventName('set_pin'),
parameters: parameters,
);
}

Map<String, Object>? storyAnalyticParameters(StoryDbModel story) {
return sanitizeParameters({
'type': story.type.name,
Expand Down
11 changes: 0 additions & 11 deletions lib/core/services/analytics/analytics_user_propery_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,4 @@ class AnalyticsUserProperyService extends BaseAnalyticsService {
value: newFontFamily,
);
}

Future<void> logSetLocalAuth({
required bool enable,
}) {
debug('logSetLocalAuth', {'value': enable.toString()});

return FirebaseAnalytics.instance.setUserProperty(
name: 'local_auth',
value: enable.toString(),
);
}
}
32 changes: 14 additions & 18 deletions lib/core/services/local_auth_service.dart
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
import 'package:local_auth/local_auth.dart';
import 'package:storypad/core/storages/local_auth_enabled_storage.dart';

class LocalAuthService {
LocalAuthService._();

static final LocalAuthService instance = LocalAuthService._();
final LocalAuthentication auth = LocalAuthentication();

bool? _localAuthEnabled;
bool? _isDeviceSupported;
bool? _canCheckBiometrics;
List<BiometricType>? enrolledBiometrics;

bool get localAuthEnabled => _localAuthEnabled!;
bool get canCheckBiometrics => _canCheckBiometrics!;

Future<void> setEnable(bool value) async {
await LocalAuthEnabledStorage().write(value);
await load();
}
bool? get canCheckBiometrics => _canCheckBiometrics;
bool get enrolledBothFingerprintAndFace => enrolledFingerprint && enrolledFace;
bool get enrolledFingerprint => enrolledBiometrics?.contains(BiometricType.fingerprint) == true;
bool get enrolledFace => enrolledBiometrics?.contains(BiometricType.face) == true;
bool get enrolledOtherBiometrics => _canCheckBiometrics == true;

Future<void> load() async {
bool isDeviceSupported = await auth.isDeviceSupported();

_canCheckBiometrics = isDeviceSupported && await auth.canCheckBiometrics;
_localAuthEnabled = await LocalAuthEnabledStorage().read() == true;
_isDeviceSupported = await auth.isDeviceSupported();
_canCheckBiometrics = await auth.canCheckBiometrics;
if (_isDeviceSupported!) enrolledBiometrics = await auth.getAvailableBiometrics();
}

Future<bool> authenticate() async {
Future<bool> authenticate({
required String title,
}) async {
await auth.stopAuthentication();
return auth.authenticate(
localizedReason: 'Unlock to open the app',
localizedReason: title,
options: const AuthenticationOptions(
stickyAuth: true,
),
Expand Down
36 changes: 36 additions & 0 deletions lib/core/storages/app_lock_storage.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:storypad/core/objects/app_lock_object.dart';
import 'package:storypad/core/storages/base_object_storages/object_storage.dart';
import 'package:storypad/core/storages/base_object_storages/bool_storage.dart';

class AppLockStorage extends ObjectStorage<AppLockObject> {
@override
AppLockObject decode(Map<String, dynamic> json) {
return AppLockObject.fromJson(json);
}

@override
Map<String, dynamic> encode(AppLockObject object) {
return object.toJson();
}

@override
Future<AppLockObject?> readObject() async {
AppLockObject? data = await super.readObject();

// TODO: remove this after a while.
// ignore: deprecated_member_use_from_same_package
bool? deprecatedData = await LocalAuthEnabledStorage().read();

if (deprecatedData != null) {
data = (data ?? AppLockObject.init()).copyWith(enabledBiometric: deprecatedData);

// ignore: deprecated_member_use_from_same_package
LocalAuthEnabledStorage().remove();
}

return data;
}
}

@Deprecated('Will be removed soon.')
class LocalAuthEnabledStorage extends BoolStorage {}
3 changes: 0 additions & 3 deletions lib/core/storages/local_auth_enabled_storage.dart

This file was deleted.

32 changes: 32 additions & 0 deletions lib/core/types/app_lock_question.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// ignore_for_file: constant_identifier_names

import 'package:easy_localization/easy_localization.dart' show tr;

enum AppLockQuestion {
name_of_your_first_pet,
city_or_town_your_were_born,
favorite_childhood_friend,
favorite_color,
name_of_elementary_school,
city_or_town_your_parent_met,
name_of_your_first_teacher;

String get translatedQuestion {
switch (this) {
case name_of_your_first_pet:
return tr('general.security_question.name_of_your_first_pet');
case city_or_town_your_were_born:
return tr('general.security_question.city_or_town_your_were_born');
case favorite_childhood_friend:
return tr('general.security_question.favorite_childhood_friend');
case favorite_color:
return tr('general.security_question.favorite_color');
case name_of_elementary_school:
return tr('general.security_question.name_of_elementary_school');
case city_or_town_your_parent_met:
return tr('general.security_question.city_or_town_your_parent_met');
case name_of_your_first_teacher:
return tr('general.security_question.name_of_your_first_teacher');
}
}
}
36 changes: 36 additions & 0 deletions lib/initializers/app_lock_initializer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// ignore_for_file: library_private_types_in_public_api

import 'package:storypad/core/objects/app_lock_object.dart';
import 'package:storypad/core/services/local_auth_service.dart';
import 'package:storypad/core/storages/app_lock_storage.dart';

class _AppLockInitialData {
final LocalAuthService localAuth;
final AppLockObject appLock;

_AppLockInitialData({
required this.localAuth,
required this.appLock,
});
}

class AppLockInitializer {
static _AppLockInitialData? _initialData;

static Future<void> call() async {
final localAuth = LocalAuthService();
await localAuth.load();
final appLock = await AppLockStorage().readObject() ?? AppLockObject.init();

_initialData = _AppLockInitialData(
localAuth: localAuth,
appLock: appLock,
);
}

static _AppLockInitialData? getAndClear() {
final tmp = _initialData;
_initialData = null;
return tmp;
}
}
7 changes: 0 additions & 7 deletions lib/initializers/local_auth_initializer.dart

This file was deleted.

4 changes: 2 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import 'package:easy_localization/easy_localization.dart' show EasyLocalization;
import 'package:flutter/material.dart' show WidgetsBinding, WidgetsFlutterBinding, runApp;
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:storypad/app.dart' show App;
import 'package:storypad/initializers/app_lock_initializer.dart';
import 'package:storypad/initializers/database_initializer.dart' show DatabaseInitializer;
import 'package:storypad/initializers/device_info_initializer.dart' show DeviceInfoInitializer;
import 'package:storypad/initializers/file_initializer.dart' show FileInitializer;
import 'package:storypad/initializers/firebase_crashlytics_initializer.dart' show FirebaseCrashlyticsInitializer;
import 'package:storypad/initializers/firebase_remote_config_initializer.dart' show FirebaseRemoteConfigInitializer;
import 'package:storypad/initializers/home_initializer.dart';
import 'package:storypad/initializers/licenses_initializer.dart' show LicensesInitializer;
import 'package:storypad/initializers/local_auth_initializer.dart' show LocalAuthInitializer;
import 'package:storypad/initializers/package_info_initializer.dart' show PackageInfoInitializer;
import 'package:storypad/initializers/theme_initializer.dart' show ThemeInitializer;
import 'package:storypad/provider_scope.dart' show ProviderScope;
Expand All @@ -27,8 +27,8 @@ void main({
await DeviceInfoInitializer.call();
await FileInitializer.call();
await DatabaseInitializer.call();
await LocalAuthInitializer.call();
await HomeInitializer.call();
await AppLockInitializer.call();

FirebaseCrashlyticsInitializer.call();
FirebaseRemoteConfigInitializer.call();
Expand Down
Loading

0 comments on commit 6a3f899

Please sign in to comment.