From 544328fa33b5edad20417fe0dbbf09b07e0c7462 Mon Sep 17 00:00:00 2001 From: YeungKC Date: Thu, 6 Jul 2023 17:33:10 +0900 Subject: [PATCH] feat: Screen lock (#1276) * Feat: Screen lock * update * update * l10n * update * update * update * update --- assets/images/lock.svg | 8 + assets/images/proxy.svg | 5 + assets/images/shield.svg | 5 + lib/account/security_key_value.dart | 38 +++ lib/app.dart | 5 +- lib/constants/resources.dart | 9 + lib/generated/intl/messages_en.dart | 12 + lib/generated/intl/messages_es.dart | 1 + lib/generated/intl/messages_in.dart | 1 + lib/generated/intl/messages_ja.dart | 1 + lib/generated/intl/messages_ms.dart | 1 + lib/generated/intl/messages_ru.dart | 1 + lib/generated/intl/messages_zh-HK.dart | 8 + lib/generated/intl/messages_zh-TW.dart | 8 + lib/generated/intl/messages_zh.dart | 8 + lib/generated/l10n.dart | 70 ++++++ lib/l10n/intl_en.arb | 7 + lib/l10n/intl_es.arb | 1 + lib/l10n/intl_in.arb | 1 + lib/l10n/intl_ja.arb | 1 + lib/l10n/intl_ms.arb | 1 + lib/l10n/intl_ru.arb | 1 + lib/l10n/intl_zh-HK.arb | 7 + lib/l10n/intl_zh-TW.arb | 7 + lib/l10n/intl_zh.arb | 7 + .../route/responsive_navigator_cubit.dart | 11 + lib/ui/setting/appearance_page.dart | 6 +- lib/ui/setting/security_page.dart | 178 +++++++++++++ lib/ui/setting/setting_page.dart | 12 +- lib/utils/file.dart | 5 +- lib/utils/hive_key_values.dart | 3 + lib/widgets/auth.dart | 235 ++++++++++++++++++ lib/widgets/dialog.dart | 7 +- lib/widgets/toast.dart | 6 +- lib/widgets/window/menus.dart | 22 ++ macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- pubspec.yaml | 10 +- 38 files changed, 690 insertions(+), 23 deletions(-) create mode 100644 assets/images/lock.svg create mode 100644 assets/images/proxy.svg create mode 100644 assets/images/shield.svg create mode 100644 lib/account/security_key_value.dart create mode 100644 lib/ui/setting/security_page.dart create mode 100644 lib/widgets/auth.dart diff --git a/assets/images/lock.svg b/assets/images/lock.svg new file mode 100644 index 0000000000..929c8d35f1 --- /dev/null +++ b/assets/images/lock.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/assets/images/proxy.svg b/assets/images/proxy.svg new file mode 100644 index 0000000000..4e67faa68f --- /dev/null +++ b/assets/images/proxy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/shield.svg b/assets/images/shield.svg new file mode 100644 index 0000000000..8f5134acff --- /dev/null +++ b/assets/images/shield.svg @@ -0,0 +1,5 @@ + + + diff --git a/lib/account/security_key_value.dart b/lib/account/security_key_value.dart new file mode 100644 index 0000000000..b138826e6c --- /dev/null +++ b/lib/account/security_key_value.dart @@ -0,0 +1,38 @@ +import 'package:rxdart/rxdart.dart'; + +import '../utils/hive_key_values.dart'; + +class SecurityKeyValue extends HiveKeyValue { + SecurityKeyValue._() : super(_hiveSecurity); + + static SecurityKeyValue? _instance; + + static SecurityKeyValue get instance => _instance ??= SecurityKeyValue._(); + + static const _hiveSecurity = 'security_box'; + static const _passcode = 'passcode'; + static const _biometric = 'biometric'; + + String? get passcode => box.get(_passcode) as String?; + + set passcode(String? value) { + if (value != null && value.length != 6) { + throw ArgumentError('Passcode must be 6 digits'); + } + box.put(_passcode, value); + } + + bool get biometric => box.get(_biometric, defaultValue: false) as bool; + + set biometric(bool value) => box.put(_biometric, value); + + bool get hasPasscode => passcode != null; + + Stream watchHasPasscode() => box + .watch(key: _passcode) + .map((event) => event.value != null) + .startWith(passcode != null); + + Stream watchBiometric() => + box.watch(key: _biometric).map((event) => (event.value ?? false) as bool); +} diff --git a/lib/app.dart b/lib/app.dart index 20e9782e1c..ec8383f6fc 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -34,6 +34,7 @@ import 'utils/platform.dart'; import 'utils/system/system_fonts.dart'; import 'utils/system/text_input.dart'; import 'utils/system/tray.dart'; +import 'widgets/auth.dart'; import 'widgets/brightness_observer.dart'; import 'widgets/focus_helper.dart'; import 'widgets/message/item/text/mention_builder.dart'; @@ -243,7 +244,9 @@ class _App extends StatelessWidget { ), child: SystemTrayWidget( child: TextInputActionHandler( - child: child!, + child: AuthGuard( + child: child!, + ), ), ), ), diff --git a/lib/constants/resources.dart b/lib/constants/resources.dart index 5156e8320e..57552c8fb3 100644 --- a/lib/constants/resources.dart +++ b/lib/constants/resources.dart @@ -425,6 +425,9 @@ class Resources { static const String assetsImagesLocationMarkSvg = 'assets/images/location_mark.svg'; + /// {@macro assets_generator.assetsImagesLockSvg.preview} + static const String assetsImagesLockSvg = 'assets/images/lock.svg'; + /// {@macro assets_generator.assetsImagesLogoPng.preview} static const String assetsImagesLogoPng = 'assets/images/logo.png'; @@ -493,6 +496,9 @@ class Resources { /// {@macro assets_generator.assetsImagesPrevSvg.preview} static const String assetsImagesPrevSvg = 'assets/images/prev.svg'; + /// {@macro assets_generator.assetsImagesProxySvg.preview} + static const String assetsImagesProxySvg = 'assets/images/proxy.svg'; + /// {@macro assets_generator.assetsImagesReadSvg.preview} static const String assetsImagesReadSvg = 'assets/images/read.svg'; @@ -534,6 +540,9 @@ class Resources { /// {@macro assets_generator.assetsImagesShareSvg.preview} static const String assetsImagesShareSvg = 'assets/images/share.svg'; + /// {@macro assets_generator.assetsImagesShieldSvg.preview} + static const String assetsImagesShieldSvg = 'assets/images/shield.svg'; + /// {@macro assets_generator.assetsImagesSlideContactsSvg.preview} static const String assetsImagesSlideContactsSvg = 'assets/images/slide_contacts.svg'; diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 4c4d3cf15c..a37e76923a 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -243,6 +243,8 @@ class MessageLookup extends MessageLookupByLibrary { "combineAndForward": MessageLookupByLibrary.simpleMessage("Combine and forward"), "confirm": MessageLookupByLibrary.simpleMessage("Confirm"), + "confirmPasscodeDesc": MessageLookupByLibrary.simpleMessage( + "Enter again to confirm the passcode"), "confirmSyncChatsFromPhone": MessageLookupByLibrary.simpleMessage( "Are you sure to sync the chat history from the phone?"), "confirmSyncChatsToPhone": MessageLookupByLibrary.simpleMessage( @@ -487,6 +489,7 @@ class MessageLookup extends MessageLookupByLibrary { "System time is unusual, please continue to use again after correction"), "locateToChat": MessageLookupByLibrary.simpleMessage("locate to chat"), "location": MessageLookupByLibrary.simpleMessage("Location"), + "lock": MessageLookupByLibrary.simpleMessage("Lock"), "logIn": MessageLookupByLibrary.simpleMessage("Log in"), "loginAndAbortAccountDeletion": MessageLookupByLibrary.simpleMessage( "Continue to log in and abort account deletion"), @@ -563,6 +566,8 @@ class MessageLookup extends MessageLookupByLibrary { "originalImage": MessageLookupByLibrary.simpleMessage("Original"), "owner": MessageLookupByLibrary.simpleMessage("Owner"), "participantsCount": m35, + "passcodeIncorrect": + MessageLookupByLibrary.simpleMessage("Passcode incorrect"), "password": MessageLookupByLibrary.simpleMessage("Password"), "pendingConfirmation": m36, "phoneNumber": MessageLookupByLibrary.simpleMessage("Phone Number"), @@ -620,6 +625,8 @@ class MessageLookup extends MessageLookupByLibrary { "sayHi": MessageLookupByLibrary.simpleMessage("Say Hi"), "scamWarning": MessageLookupByLibrary.simpleMessage( "Warning: Many users reported this account as a scam. Please be careful, especially if it asks you for money"), + "screenPasscode": + MessageLookupByLibrary.simpleMessage("Screen Passcode"), "search": MessageLookupByLibrary.simpleMessage("Search"), "searchContact": MessageLookupByLibrary.simpleMessage("Search contact"), "searchConversation": @@ -632,6 +639,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchUnread": MessageLookupByLibrary.simpleMessage("Search Unread"), "secretUrl": MessageLookupByLibrary.simpleMessage( "https://mixin.one/pages/1000007"), + "security": MessageLookupByLibrary.simpleMessage("Security"), "select": MessageLookupByLibrary.simpleMessage("Select"), "send": MessageLookupByLibrary.simpleMessage("Send"), "sendArchived": MessageLookupByLibrary.simpleMessage( @@ -645,6 +653,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Send Without Sound"), "set": MessageLookupByLibrary.simpleMessage("Set"), "setDisappearingMessageTimeTo": m40, + "setPasscodeDesc": MessageLookupByLibrary.simpleMessage( + "Set Passcode to unlock Mixin Messenger"), "settingAuthSearchHint": MessageLookupByLibrary.simpleMessage("Mixin ID, Name"), "settingBackupTips": MessageLookupByLibrary.simpleMessage( @@ -729,6 +739,8 @@ class MessageLookup extends MessageLookupByLibrary { "unitSecond": m48, "unitWeek": m49, "unknowError": MessageLookupByLibrary.simpleMessage("Unknow error"), + "unlockWithWasscode": MessageLookupByLibrary.simpleMessage( + "Enter Passcode to unlock Mixin Messenger"), "unmute": MessageLookupByLibrary.simpleMessage("Unmute"), "unpin": MessageLookupByLibrary.simpleMessage("Unpin"), "unpinAllMessages": diff --git a/lib/generated/intl/messages_es.dart b/lib/generated/intl/messages_es.dart index 8623c33a90..69bcccda15 100644 --- a/lib/generated/intl/messages_es.dart +++ b/lib/generated/intl/messages_es.dart @@ -634,6 +634,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchUnread": MessageLookupByLibrary.simpleMessage("Buscar no leído"), "secretUrl": MessageLookupByLibrary.simpleMessage( "https://mixin.one/pages/1000007"), + "security": MessageLookupByLibrary.simpleMessage("Seguridad"), "select": MessageLookupByLibrary.simpleMessage("Seleccionar"), "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendArchived": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_in.dart b/lib/generated/intl/messages_in.dart index 83b8327d7b..c26333704b 100644 --- a/lib/generated/intl/messages_in.dart +++ b/lib/generated/intl/messages_in.dart @@ -342,6 +342,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchRelatedMessage": m39, "secretUrl": MessageLookupByLibrary.simpleMessage( "https://mixin.one/pages/1000007"), + "security": MessageLookupByLibrary.simpleMessage("Keamanan"), "select": MessageLookupByLibrary.simpleMessage("Pilih"), "send": MessageLookupByLibrary.simpleMessage("Kirim"), "settingAuthSearchHint": diff --git a/lib/generated/intl/messages_ja.dart b/lib/generated/intl/messages_ja.dart index bb488910a1..c2ff477b75 100644 --- a/lib/generated/intl/messages_ja.dart +++ b/lib/generated/intl/messages_ja.dart @@ -533,6 +533,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchRelatedMessage": m39, "secretUrl": MessageLookupByLibrary.simpleMessage( "https://mixin.one/pages/1000007"), + "security": MessageLookupByLibrary.simpleMessage("セキュリティ"), "select": MessageLookupByLibrary.simpleMessage("選択"), "send": MessageLookupByLibrary.simpleMessage("送る"), "sendArchived": diff --git a/lib/generated/intl/messages_ms.dart b/lib/generated/intl/messages_ms.dart index c99af5bfdb..2822db1f9a 100644 --- a/lib/generated/intl/messages_ms.dart +++ b/lib/generated/intl/messages_ms.dart @@ -348,6 +348,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchRelatedMessage": m39, "secretUrl": MessageLookupByLibrary.simpleMessage( "https://mixin.one/pages/1000007"), + "security": MessageLookupByLibrary.simpleMessage("Keselamatan"), "select": MessageLookupByLibrary.simpleMessage("Pilih"), "send": MessageLookupByLibrary.simpleMessage("Hantar"), "settingAuthSearchHint": diff --git a/lib/generated/intl/messages_ru.dart b/lib/generated/intl/messages_ru.dart index 562827cc80..b4b8a08463 100644 --- a/lib/generated/intl/messages_ru.dart +++ b/lib/generated/intl/messages_ru.dart @@ -597,6 +597,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchRelatedMessage": m39, "secretUrl": MessageLookupByLibrary.simpleMessage( "https://mixin.one/pages/1000007"), + "security": MessageLookupByLibrary.simpleMessage("Безопасность"), "select": MessageLookupByLibrary.simpleMessage("Выбрать"), "send": MessageLookupByLibrary.simpleMessage("Отправить"), "sendArchived": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_zh-HK.dart b/lib/generated/intl/messages_zh-HK.dart index 90140f1c36..ff81ba3cbd 100644 --- a/lib/generated/intl/messages_zh-HK.dart +++ b/lib/generated/intl/messages_zh-HK.dart @@ -216,6 +216,7 @@ class MessageLookup extends MessageLookupByLibrary { "collapse": MessageLookupByLibrary.simpleMessage("摺疊"), "combineAndForward": MessageLookupByLibrary.simpleMessage("合併轉發"), "confirm": MessageLookupByLibrary.simpleMessage("確認"), + "confirmPasscodeDesc": MessageLookupByLibrary.simpleMessage("再次確認密碼"), "confirmSyncChatsFromPhone": MessageLookupByLibrary.simpleMessage("確認從手機端同步聊天記錄嗎?"), "confirmSyncChatsToPhone": @@ -428,6 +429,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("檢測到系統時間異常,請校正後再繼續使用"), "locateToChat": MessageLookupByLibrary.simpleMessage("定位到聊天"), "location": MessageLookupByLibrary.simpleMessage("位置"), + "lock": MessageLookupByLibrary.simpleMessage("鎖定"), "logIn": MessageLookupByLibrary.simpleMessage("登錄"), "loginAndAbortAccountDeletion": MessageLookupByLibrary.simpleMessage("繼續登錄並放棄刪除賬户"), @@ -494,6 +496,7 @@ class MessageLookup extends MessageLookupByLibrary { "originalImage": MessageLookupByLibrary.simpleMessage("原圖"), "owner": MessageLookupByLibrary.simpleMessage("羣主"), "participantsCount": m35, + "passcodeIncorrect": MessageLookupByLibrary.simpleMessage("密碼不正確"), "password": MessageLookupByLibrary.simpleMessage("密碼"), "pendingConfirmation": m36, "phoneNumber": MessageLookupByLibrary.simpleMessage("手機號碼"), @@ -543,6 +546,7 @@ class MessageLookup extends MessageLookupByLibrary { "sayHi": MessageLookupByLibrary.simpleMessage("打招呼"), "scamWarning": MessageLookupByLibrary.simpleMessage( "警告:此賬號被大量用户舉報,請謹防網絡詐騙,注意個人財產安全"), + "screenPasscode": MessageLookupByLibrary.simpleMessage("鎖屏密碼"), "search": MessageLookupByLibrary.simpleMessage("搜索"), "searchContact": MessageLookupByLibrary.simpleMessage("搜索用户"), "searchConversation": MessageLookupByLibrary.simpleMessage("搜索聊天記錄"), @@ -551,6 +555,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("搜索 Mixin ID 或手機號碼:"), "searchRelatedMessage": m39, "searchUnread": MessageLookupByLibrary.simpleMessage("搜索未讀會話"), + "security": MessageLookupByLibrary.simpleMessage("安全"), "select": MessageLookupByLibrary.simpleMessage("選擇"), "send": MessageLookupByLibrary.simpleMessage("發送"), "sendArchived": MessageLookupByLibrary.simpleMessage("打包成 zip 發送"), @@ -561,6 +566,7 @@ class MessageLookup extends MessageLookupByLibrary { "sendWithoutSound": MessageLookupByLibrary.simpleMessage("靜音發送"), "set": MessageLookupByLibrary.simpleMessage("設置"), "setDisappearingMessageTimeTo": m40, + "setPasscodeDesc": MessageLookupByLibrary.simpleMessage("設置解鎖密碼"), "settingAuthSearchHint": MessageLookupByLibrary.simpleMessage("Mixin ID, 暱稱"), "settingBackupTips": MessageLookupByLibrary.simpleMessage( @@ -627,6 +633,8 @@ class MessageLookup extends MessageLookupByLibrary { "unitSecond": m48, "unitWeek": m49, "unknowError": MessageLookupByLibrary.simpleMessage("未知錯誤"), + "unlockWithWasscode": + MessageLookupByLibrary.simpleMessage("輸入密碼解鎖 Mixin Messenger"), "unmute": MessageLookupByLibrary.simpleMessage("取消靜音"), "unpin": MessageLookupByLibrary.simpleMessage("取消置頂"), "unpinAllMessages": MessageLookupByLibrary.simpleMessage("取消所有置頂消息"), diff --git a/lib/generated/intl/messages_zh-TW.dart b/lib/generated/intl/messages_zh-TW.dart index 903b0daf88..91d80276dd 100644 --- a/lib/generated/intl/messages_zh-TW.dart +++ b/lib/generated/intl/messages_zh-TW.dart @@ -216,6 +216,7 @@ class MessageLookup extends MessageLookupByLibrary { "collapse": MessageLookupByLibrary.simpleMessage("摺疊"), "combineAndForward": MessageLookupByLibrary.simpleMessage("合併轉發"), "confirm": MessageLookupByLibrary.simpleMessage("確認"), + "confirmPasscodeDesc": MessageLookupByLibrary.simpleMessage("再次確認密碼"), "confirmSyncChatsFromPhone": MessageLookupByLibrary.simpleMessage("確認從手機端同步聊天記錄嗎?"), "confirmSyncChatsToPhone": @@ -428,6 +429,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("檢測到系統時間異常,請校正後再繼續使用"), "locateToChat": MessageLookupByLibrary.simpleMessage("定位到聊天"), "location": MessageLookupByLibrary.simpleMessage("位置"), + "lock": MessageLookupByLibrary.simpleMessage("鎖定"), "logIn": MessageLookupByLibrary.simpleMessage("登入"), "loginAndAbortAccountDeletion": MessageLookupByLibrary.simpleMessage("繼續登入並放棄刪除賬戶"), @@ -494,6 +496,7 @@ class MessageLookup extends MessageLookupByLibrary { "originalImage": MessageLookupByLibrary.simpleMessage("原圖"), "owner": MessageLookupByLibrary.simpleMessage("群主"), "participantsCount": m35, + "passcodeIncorrect": MessageLookupByLibrary.simpleMessage("密碼不正確"), "password": MessageLookupByLibrary.simpleMessage("密碼"), "pendingConfirmation": m36, "phoneNumber": MessageLookupByLibrary.simpleMessage("手機號碼"), @@ -543,6 +546,7 @@ class MessageLookup extends MessageLookupByLibrary { "sayHi": MessageLookupByLibrary.simpleMessage("打招呼"), "scamWarning": MessageLookupByLibrary.simpleMessage( "警告:此賬號被大量使用者舉報,請謹防網路詐騙,注意個人財產安全"), + "screenPasscode": MessageLookupByLibrary.simpleMessage("鎖屏密碼"), "search": MessageLookupByLibrary.simpleMessage("搜尋"), "searchContact": MessageLookupByLibrary.simpleMessage("搜尋使用者"), "searchConversation": MessageLookupByLibrary.simpleMessage("搜尋聊天記錄"), @@ -551,6 +555,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("搜尋 Mixin ID 或手機號碼:"), "searchRelatedMessage": m39, "searchUnread": MessageLookupByLibrary.simpleMessage("搜尋未讀會話"), + "security": MessageLookupByLibrary.simpleMessage("安全"), "select": MessageLookupByLibrary.simpleMessage("選擇"), "send": MessageLookupByLibrary.simpleMessage("傳送"), "sendArchived": MessageLookupByLibrary.simpleMessage("打包成 zip 傳送"), @@ -561,6 +566,7 @@ class MessageLookup extends MessageLookupByLibrary { "sendWithoutSound": MessageLookupByLibrary.simpleMessage("靜音傳送"), "set": MessageLookupByLibrary.simpleMessage("設定"), "setDisappearingMessageTimeTo": m40, + "setPasscodeDesc": MessageLookupByLibrary.simpleMessage("設定解鎖密碼"), "settingAuthSearchHint": MessageLookupByLibrary.simpleMessage("Mixin ID, 暱稱"), "settingBackupTips": MessageLookupByLibrary.simpleMessage( @@ -627,6 +633,8 @@ class MessageLookup extends MessageLookupByLibrary { "unitSecond": m48, "unitWeek": m49, "unknowError": MessageLookupByLibrary.simpleMessage("未知錯誤"), + "unlockWithWasscode": + MessageLookupByLibrary.simpleMessage("輸入密碼解鎖 Mixin Messenger"), "unmute": MessageLookupByLibrary.simpleMessage("取消靜音"), "unpin": MessageLookupByLibrary.simpleMessage("取消置頂"), "unpinAllMessages": MessageLookupByLibrary.simpleMessage("取消所有置頂訊息"), diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index 916e87bc38..76dc8e2ef2 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -216,6 +216,7 @@ class MessageLookup extends MessageLookupByLibrary { "collapse": MessageLookupByLibrary.simpleMessage("折叠"), "combineAndForward": MessageLookupByLibrary.simpleMessage("合并转发"), "confirm": MessageLookupByLibrary.simpleMessage("确认"), + "confirmPasscodeDesc": MessageLookupByLibrary.simpleMessage("再次确认密码"), "confirmSyncChatsFromPhone": MessageLookupByLibrary.simpleMessage("确认从手机端同步聊天记录吗?"), "confirmSyncChatsToPhone": @@ -428,6 +429,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("检测到系统时间异常,请校正后再继续使用"), "locateToChat": MessageLookupByLibrary.simpleMessage("定位到聊天"), "location": MessageLookupByLibrary.simpleMessage("位置"), + "lock": MessageLookupByLibrary.simpleMessage("锁定"), "logIn": MessageLookupByLibrary.simpleMessage("登录"), "loginAndAbortAccountDeletion": MessageLookupByLibrary.simpleMessage("继续登录并放弃删除账户"), @@ -494,6 +496,7 @@ class MessageLookup extends MessageLookupByLibrary { "originalImage": MessageLookupByLibrary.simpleMessage("原图"), "owner": MessageLookupByLibrary.simpleMessage("群主"), "participantsCount": m35, + "passcodeIncorrect": MessageLookupByLibrary.simpleMessage("密码不正确"), "password": MessageLookupByLibrary.simpleMessage("密码"), "pendingConfirmation": m36, "phoneNumber": MessageLookupByLibrary.simpleMessage("手机号码"), @@ -543,6 +546,7 @@ class MessageLookup extends MessageLookupByLibrary { "sayHi": MessageLookupByLibrary.simpleMessage("打招呼"), "scamWarning": MessageLookupByLibrary.simpleMessage( "警告:此账号被大量用户举报,请谨防网络诈骗,注意个人财产安全"), + "screenPasscode": MessageLookupByLibrary.simpleMessage("锁屏密码"), "search": MessageLookupByLibrary.simpleMessage("搜索"), "searchContact": MessageLookupByLibrary.simpleMessage("搜索用户"), "searchConversation": MessageLookupByLibrary.simpleMessage("搜索聊天记录"), @@ -551,6 +555,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("搜索 Mixin ID 或手机号码:"), "searchRelatedMessage": m39, "searchUnread": MessageLookupByLibrary.simpleMessage("搜索未读会话"), + "security": MessageLookupByLibrary.simpleMessage("安全"), "select": MessageLookupByLibrary.simpleMessage("选择"), "send": MessageLookupByLibrary.simpleMessage("发送"), "sendArchived": MessageLookupByLibrary.simpleMessage("打包成 zip 发送"), @@ -561,6 +566,7 @@ class MessageLookup extends MessageLookupByLibrary { "sendWithoutSound": MessageLookupByLibrary.simpleMessage("静音发送"), "set": MessageLookupByLibrary.simpleMessage("设置"), "setDisappearingMessageTimeTo": m40, + "setPasscodeDesc": MessageLookupByLibrary.simpleMessage("设置解锁密码"), "settingAuthSearchHint": MessageLookupByLibrary.simpleMessage("Mixin ID, 昵称"), "settingBackupTips": MessageLookupByLibrary.simpleMessage( @@ -627,6 +633,8 @@ class MessageLookup extends MessageLookupByLibrary { "unitSecond": m48, "unitWeek": m49, "unknowError": MessageLookupByLibrary.simpleMessage("未知错误"), + "unlockWithWasscode": + MessageLookupByLibrary.simpleMessage("输入密码解锁 Mixin Messenger"), "unmute": MessageLookupByLibrary.simpleMessage("取消静音"), "unpin": MessageLookupByLibrary.simpleMessage("取消置顶"), "unpinAllMessages": MessageLookupByLibrary.simpleMessage("取消所有置顶消息"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index a68ab6341b..c95678e2cd 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -794,6 +794,16 @@ class Localization { ); } + /// `Enter again to confirm the passcode` + String get confirmPasscodeDesc { + return Intl.message( + 'Enter again to confirm the passcode', + name: 'confirmPasscodeDesc', + desc: '', + args: [], + ); + } + /// `Are you sure to sync the chat history from the phone?` String get confirmSyncChatsFromPhone { return Intl.message( @@ -2420,6 +2430,16 @@ class Localization { ); } + /// `Lock` + String get lock { + return Intl.message( + 'Lock', + name: 'lock', + desc: '', + args: [], + ); + } + /// `Log in` String get logIn { return Intl.message( @@ -2970,6 +2990,16 @@ class Localization { ); } + /// `Passcode incorrect` + String get passcodeIncorrect { + return Intl.message( + 'Passcode incorrect', + name: 'passcodeIncorrect', + desc: '', + args: [], + ); + } + /// `Password` String get password { return Intl.message( @@ -3424,6 +3454,16 @@ class Localization { ); } + /// `Screen Passcode` + String get screenPasscode { + return Intl.message( + 'Screen Passcode', + name: 'screenPasscode', + desc: '', + args: [], + ); + } + /// `Search` String get search { return Intl.message( @@ -3506,6 +3546,16 @@ class Localization { ); } + /// `Security` + String get security { + return Intl.message( + 'Security', + name: 'security', + desc: '', + args: [], + ); + } + /// `Select` String get select { return Intl.message( @@ -3596,6 +3646,16 @@ class Localization { ); } + /// `Set Passcode to unlock Mixin Messenger` + String get setPasscodeDesc { + return Intl.message( + 'Set Passcode to unlock Mixin Messenger', + name: 'setPasscodeDesc', + desc: '', + args: [], + ); + } + /// `Mixin ID, Name` String get settingAuthSearchHint { return Intl.message( @@ -4226,6 +4286,16 @@ class Localization { ); } + /// `Enter Passcode to unlock Mixin Messenger` + String get unlockWithWasscode { + return Intl.message( + 'Enter Passcode to unlock Mixin Messenger', + name: 'unlockWithWasscode', + desc: '', + args: [], + ); + } + /// `Unmute` String get unmute { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 1f5ff0567f..36f642db92 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -73,6 +73,7 @@ "collapse" : "Collapse", "combineAndForward" : "Combine and forward", "confirm" : "Confirm", +"confirmPasscodeDesc" : "Enter again to confirm the passcode", "confirmSyncChatsFromPhone" : "Are you sure to sync the chat history from the phone?", "confirmSyncChatsToPhone" : "Are you sure to sync the chat history to the phone?", "contact" : "Contact", @@ -235,6 +236,7 @@ "loadingTime" : "System time is unusual, please continue to use again after correction", "locateToChat" : "locate to chat", "location" : "Location", +"lock" : "Lock", "logIn" : "Log in", "loginAndAbortAccountDeletion" : "Continue to log in and abort account deletion", "loginByQrcode" : "Login to Mixin Messenger by QR Code", @@ -290,6 +292,7 @@ "originalImage" : "Original", "owner" : "Owner", "participantsCount" : "{arg0} PARTICIPANTS", +"passcodeIncorrect" : "Passcode incorrect", "password" : "Password", "pendingConfirmation" : "{count, plural, one{{arg0}/{arg1} confirmation} other{{arg0}/{arg1} confirmations}}", "phoneNumber" : "Phone Number", @@ -335,6 +338,7 @@ "saveToCameraRoll" : "Save to Camera Roll", "sayHi" : "Say Hi", "scamWarning" : "Warning: Many users reported this account as a scam. Please be careful, especially if it asks you for money", +"screenPasscode" : "Screen Passcode", "search" : "Search", "searchContact" : "Search contact", "searchConversation" : "Search Conversation", @@ -343,6 +347,7 @@ "searchRelatedMessage" : "{count, plural, one{{arg0} related message} other{{arg0} related messages}}", "searchUnread" : "Search Unread", "secretUrl" : "https://mixin.one/pages/1000007", +"security" : "Security", "select" : "Select", "send" : "Send", "sendArchived" : "Archived all files in one zip file", @@ -352,6 +357,7 @@ "sendWithoutSound" : "Send Without Sound", "set" : "Set", "setDisappearingMessageTimeTo" : "{arg0} set disappearing message time to {arg1}", +"setPasscodeDesc" : "Set Passcode to unlock Mixin Messenger", "settingAuthSearchHint" : "Mixin ID, Name", "settingBackupTips" : "Back up your chat history to iCloud. if you lose your iPhone or switch to a new one, you can restore your chat history when you reinstall Mixin Messenger. Messages you back up are not protected by Mixin Messenger end-to-end encryption while in iCloud.", "settingDeleteAccountPinContent" : "If you continue, your profile and account details will be delete on {arg0}. read our document to **learn more**.", @@ -414,6 +420,7 @@ "unitSecond" : "{count, plural, one{second} other{seconds}}", "unitWeek" : "{count, plural, one{week} other{weeks}}", "unknowError" : "Unknow error", +"unlockWithWasscode" : "Enter Passcode to unlock Mixin Messenger", "unmute" : "Unmute", "unpin" : "Unpin", "unpinAllMessages" : "Unpin All Messages", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 59ef41aa25..acaaeadfef 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -329,6 +329,7 @@ "searchRelatedMessage" : "{count, plural, one{{arg0} mensaje relacionado} other{{arg0} mensajes relacionados}}", "searchUnread" : "Buscar no leído", "secretUrl" : "https://mixin.one/pages/1000007", +"security" : "Seguridad", "select" : "Seleccionar", "send" : "Enviar", "sendArchived" : "Todos los archivos archivados en un archivo zip", diff --git a/lib/l10n/intl_in.arb b/lib/l10n/intl_in.arb index eb1fa4783a..dfe0e1679b 100644 --- a/lib/l10n/intl_in.arb +++ b/lib/l10n/intl_in.arb @@ -180,6 +180,7 @@ "searchConversation" : "Cari Percakapan", "searchRelatedMessage" : "{count, plural, one{null} other{{arg0} pesan terkait}}", "secretUrl" : "https://mixin.one/pages/1000007", +"security" : "Keamanan", "select" : "Pilih", "send" : "Kirim", "settingAuthSearchHint" : "Mixin ID, Nama", diff --git a/lib/l10n/intl_ja.arb b/lib/l10n/intl_ja.arb index d0a094338e..0472327986 100644 --- a/lib/l10n/intl_ja.arb +++ b/lib/l10n/intl_ja.arb @@ -309,6 +309,7 @@ "searchPlaceholderNumber" : "Mixin ID または電話番号を検索", "searchRelatedMessage" : "{count, plural, one{{arg0}個の関連するメッセージ} other{{arg0}個の関連するメッセージ}}", "secretUrl" : "https://mixin.one/pages/1000007", +"security" : "セキュリティ", "select" : "選択", "send" : "送る", "sendArchived" : "1つのZIPファイルにアーカイブ", diff --git a/lib/l10n/intl_ms.arb b/lib/l10n/intl_ms.arb index b2c3cd5a43..0138fd31e4 100644 --- a/lib/l10n/intl_ms.arb +++ b/lib/l10n/intl_ms.arb @@ -181,6 +181,7 @@ "searchConversation" : "Cari Perbualan", "searchRelatedMessage" : "{count, plural, one{null} other{{arg0} mesej berkaitan}}", "secretUrl" : "https://mixin.one/pages/1000007", +"security" : "Keselamatan", "select" : "Pilih", "send" : "Hantar", "settingAuthSearchHint" : "Mixin ID, Nama", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 18274ea512..68251e38e9 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -309,6 +309,7 @@ "searchPlaceholderNumber" : "Найдите Mixin ID или номер телефона:", "searchRelatedMessage" : "{count, plural, one{{arg0} связанное сообщение} other{{arg0} похожие сообщения}}", "secretUrl" : "https://mixin.one/pages/1000007", +"security" : "Безопасность", "select" : "Выбрать", "send" : "Отправить", "sendArchived" : "Заархивированы все файлы в один zip файл", diff --git a/lib/l10n/intl_zh-HK.arb b/lib/l10n/intl_zh-HK.arb index 76f6ae3e81..eba77747d2 100644 --- a/lib/l10n/intl_zh-HK.arb +++ b/lib/l10n/intl_zh-HK.arb @@ -72,6 +72,7 @@ "collapse" : "摺疊", "combineAndForward" : "合併轉發", "confirm" : "確認", +"confirmPasscodeDesc" : "再次確認密碼", "confirmSyncChatsFromPhone" : "確認從手機端同步聊天記錄嗎?", "confirmSyncChatsToPhone" : "確認同步聊天記錄到手機端嗎?", "contact" : "聯繫人", @@ -234,6 +235,7 @@ "loadingTime" : "檢測到系統時間異常,請校正後再繼續使用", "locateToChat" : "定位到聊天", "location" : "位置", +"lock" : "鎖定", "logIn" : "登錄", "loginAndAbortAccountDeletion" : "繼續登錄並放棄刪除賬户", "loginByQrcode" : "通過二維碼登錄 Mixin Messenger", @@ -289,6 +291,7 @@ "originalImage" : "原圖", "owner" : "羣主", "participantsCount" : "{arg0} 位羣組成員", +"passcodeIncorrect" : "密碼不正確", "password" : "密碼", "pendingConfirmation" : "{count, plural, one{{arg0}/{arg1} 區塊確認數} other{{arg0}/{arg1} 區塊確認數}}", "phoneNumber" : "手機號碼", @@ -334,6 +337,7 @@ "saveToCameraRoll" : "保存到相冊", "sayHi" : "打招呼", "scamWarning" : "警告:此賬號被大量用户舉報,請謹防網絡詐騙,注意個人財產安全", +"screenPasscode" : "鎖屏密碼", "search" : "搜索", "searchContact" : "搜索用户", "searchConversation" : "搜索聊天記錄", @@ -341,6 +345,7 @@ "searchPlaceholderNumber" : "搜索 Mixin ID 或手機號碼:", "searchRelatedMessage" : "{count, plural, one{{arg0} 條相關的消息} other{{arg0} 條相關的消息}}", "searchUnread" : "搜索未讀會話", +"security" : "安全", "select" : "選擇", "send" : "發送", "sendArchived" : "打包成 zip 發送", @@ -350,6 +355,7 @@ "sendWithoutSound" : "靜音發送", "set" : "設置", "setDisappearingMessageTimeTo" : "{arg0}將限時消息設置為 {arg1}", +"setPasscodeDesc" : "設置解鎖密碼", "settingAuthSearchHint" : "Mixin ID, 暱稱", "settingBackupTips" : "備份你的聊天記錄到 iCloud。如果你丟失或者更換手機,你可以在重新安裝 Mixin Messenger 時恢復你的聊天記錄。注意備份到 iCloud 中的聊天記錄不受端對端加密保護!", "settingDeleteAccountPinContent" : "如果您繼續,您的個人資料和賬户信息將在{arg0}被刪除。閲讀我們的文檔以**瞭解更多**。", @@ -410,6 +416,7 @@ "unitSecond" : "{count, plural, one{秒} other{秒}}", "unitWeek" : "{count, plural, one{週} other{週}}", "unknowError" : "未知錯誤", +"unlockWithWasscode" : "輸入密碼解鎖 Mixin Messenger", "unmute" : "取消靜音", "unpin" : "取消置頂", "unpinAllMessages" : "取消所有置頂消息", diff --git a/lib/l10n/intl_zh-TW.arb b/lib/l10n/intl_zh-TW.arb index a4f4e1f24d..2f4d30a950 100644 --- a/lib/l10n/intl_zh-TW.arb +++ b/lib/l10n/intl_zh-TW.arb @@ -72,6 +72,7 @@ "collapse" : "摺疊", "combineAndForward" : "合併轉發", "confirm" : "確認", +"confirmPasscodeDesc" : "再次確認密碼", "confirmSyncChatsFromPhone" : "確認從手機端同步聊天記錄嗎?", "confirmSyncChatsToPhone" : "確認同步聊天記錄到手機端嗎?", "contact" : "聯絡人", @@ -234,6 +235,7 @@ "loadingTime" : "檢測到系統時間異常,請校正後再繼續使用", "locateToChat" : "定位到聊天", "location" : "位置", +"lock" : "鎖定", "logIn" : "登入", "loginAndAbortAccountDeletion" : "繼續登入並放棄刪除賬戶", "loginByQrcode" : "透過二維碼登入 Mixin Messenger", @@ -289,6 +291,7 @@ "originalImage" : "原圖", "owner" : "群主", "participantsCount" : "{arg0} 位群組成員", +"passcodeIncorrect" : "密碼不正確", "password" : "密碼", "pendingConfirmation" : "{count, plural, one{{arg0}/{arg1} 區塊確認數} other{{arg0}/{arg1} 區塊確認數}}", "phoneNumber" : "手機號碼", @@ -334,6 +337,7 @@ "saveToCameraRoll" : "儲存到相簿", "sayHi" : "打招呼", "scamWarning" : "警告:此賬號被大量使用者舉報,請謹防網路詐騙,注意個人財產安全", +"screenPasscode" : "鎖屏密碼", "search" : "搜尋", "searchContact" : "搜尋使用者", "searchConversation" : "搜尋聊天記錄", @@ -341,6 +345,7 @@ "searchPlaceholderNumber" : "搜尋 Mixin ID 或手機號碼:", "searchRelatedMessage" : "{count, plural, one{{arg0} 條相關的訊息} other{{arg0} 條相關的訊息}}", "searchUnread" : "搜尋未讀會話", +"security" : "安全", "select" : "選擇", "send" : "傳送", "sendArchived" : "打包成 zip 傳送", @@ -350,6 +355,7 @@ "sendWithoutSound" : "靜音傳送", "set" : "設定", "setDisappearingMessageTimeTo" : "{arg0}將限時訊息設定為 {arg1}", +"setPasscodeDesc" : "設定解鎖密碼", "settingAuthSearchHint" : "Mixin ID, 暱稱", "settingBackupTips" : "備份你的聊天記錄到 iCloud。如果你丟失或者更換手機,你可以在重新安裝 Mixin Messenger 時恢復你的聊天記錄。注意備份到 iCloud 中的聊天記錄不受端對端加密保護!", "settingDeleteAccountPinContent" : "如果您繼續,您的個人資料和賬戶資訊將在{arg0}被刪除。閱讀我們的檔案以**瞭解更多**。", @@ -410,6 +416,7 @@ "unitSecond" : "{count, plural, one{秒} other{秒}}", "unitWeek" : "{count, plural, one{週} other{週}}", "unknowError" : "未知錯誤", +"unlockWithWasscode" : "輸入密碼解鎖 Mixin Messenger", "unmute" : "取消靜音", "unpin" : "取消置頂", "unpinAllMessages" : "取消所有置頂訊息", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index de1483733b..77d9efe318 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -72,6 +72,7 @@ "collapse" : "折叠", "combineAndForward" : "合并转发", "confirm" : "确认", +"confirmPasscodeDesc" : "再次确认密码", "confirmSyncChatsFromPhone" : "确认从手机端同步聊天记录吗?", "confirmSyncChatsToPhone" : "确认同步聊天记录到手机端吗?", "contact" : "联系人", @@ -234,6 +235,7 @@ "loadingTime" : "检测到系统时间异常,请校正后再继续使用", "locateToChat" : "定位到聊天", "location" : "位置", +"lock" : "锁定", "logIn" : "登录", "loginAndAbortAccountDeletion" : "继续登录并放弃删除账户", "loginByQrcode" : "通过二维码登录 Mixin Messenger", @@ -289,6 +291,7 @@ "originalImage" : "原图", "owner" : "群主", "participantsCount" : "{arg0} 位群组成员", +"passcodeIncorrect" : "密码不正确", "password" : "密码", "pendingConfirmation" : "{count, plural, one{{arg0}/{arg1} 区块确认数} other{{arg0}/{arg1} 区块确认数}}", "phoneNumber" : "手机号码", @@ -334,6 +337,7 @@ "saveToCameraRoll" : "保存到相册", "sayHi" : "打招呼", "scamWarning" : "警告:此账号被大量用户举报,请谨防网络诈骗,注意个人财产安全", +"screenPasscode" : "锁屏密码", "search" : "搜索", "searchContact" : "搜索用户", "searchConversation" : "搜索聊天记录", @@ -341,6 +345,7 @@ "searchPlaceholderNumber" : "搜索 Mixin ID 或手机号码:", "searchRelatedMessage" : "{count, plural, one{{arg0} 条相关的消息} other{{arg0} 条相关的消息}}", "searchUnread" : "搜索未读会话", +"security" : "安全", "select" : "选择", "send" : "发送", "sendArchived" : "打包成 zip 发送", @@ -350,6 +355,7 @@ "sendWithoutSound" : "静音发送", "set" : "设置", "setDisappearingMessageTimeTo" : "{arg0}将限时消息设置为 {arg1}", +"setPasscodeDesc" : "设置解锁密码", "settingAuthSearchHint" : "Mixin ID, 昵称", "settingBackupTips" : "备份你的聊天记录到 iCloud。如果你丢失或者更换手机,你可以在重新安装 Mixin Messenger 时恢复你的聊天记录。注意备份到 iCloud 中的聊天记录不受端对端加密保护!", "settingDeleteAccountPinContent" : "如果您继续,您的个人资料和账户信息将在{arg0}被删除。阅读我们的文档以**了解更多**。", @@ -410,6 +416,7 @@ "unitSecond" : "{count, plural, one{秒} other{秒}}", "unitWeek" : "{count, plural, one{周} other{周}}", "unknowError" : "未知错误", +"unlockWithWasscode" : "输入密码解锁 Mixin Messenger", "unmute" : "取消静音", "unpin" : "取消置顶", "unpinAllMessages" : "取消所有置顶消息", diff --git a/lib/ui/home/route/responsive_navigator_cubit.dart b/lib/ui/home/route/responsive_navigator_cubit.dart index 998be342e6..9aa8a112fd 100644 --- a/lib/ui/home/route/responsive_navigator_cubit.dart +++ b/lib/ui/home/route/responsive_navigator_cubit.dart @@ -9,6 +9,7 @@ import '../../setting/backup_page.dart'; import '../../setting/edit_profile_page.dart'; import '../../setting/notification_page.dart'; import '../../setting/proxy_page.dart'; +import '../../setting/security_page.dart'; import '../../setting/storage_page.dart'; import '../../setting/storage_usage_detail_page.dart'; import '../../setting/storage_usage_list_page.dart'; @@ -35,6 +36,7 @@ class ResponsiveNavigatorCubit extends AbstractResponsiveNavigatorCubit { static const storageUsage = 'storageUsage'; static const storageUsageDetail = 'storageUsageDetail'; static const proxyPage = 'proxyPage'; + static const securityPage = 'securityPage'; static const settingPageNameSet = { editProfilePage, @@ -48,6 +50,7 @@ class ResponsiveNavigatorCubit extends AbstractResponsiveNavigatorCubit { accountPage, accountDeletePage, proxyPage, + securityPage, }; @override @@ -154,6 +157,14 @@ class ResponsiveNavigatorCubit extends AbstractResponsiveNavigatorCubit { key: ValueKey(proxyPage), ), ); + case securityPage: + return const MaterialPage( + key: ValueKey(securityPage), + name: securityPage, + child: SecurityPage( + key: ValueKey(securityPage), + ), + ); default: throw ArgumentError('Invalid route'); } diff --git a/lib/ui/setting/appearance_page.dart b/lib/ui/setting/appearance_page.dart index 854457d38a..6c48167b94 100644 --- a/lib/ui/setting/appearance_page.dart +++ b/lib/ui/setting/appearance_page.dart @@ -41,7 +41,7 @@ class _Body extends StatelessWidget { @override Widget build(BuildContext context) => SingleChildScrollView( - child: Container( + child: Container( padding: const EdgeInsets.only(top: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -100,7 +100,9 @@ class _Body extends StatelessWidget { const _MessageAvatarSetting(), const _ChatTextSizeSetting(), ], - ))); + ), + ), + ); } class _MessageAvatarSetting extends HookWidget { diff --git a/lib/ui/setting/security_page.dart b/lib/ui/setting/security_page.dart new file mode 100644 index 0000000000..dac84823e3 --- /dev/null +++ b/lib/ui/setting/security_page.dart @@ -0,0 +1,178 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:pin_code_fields/pin_code_fields.dart'; + +import '../../account/security_key_value.dart'; +import '../../utils/extension/extension.dart'; +import '../../widgets/app_bar.dart'; +import '../../widgets/buttons.dart'; +import '../../widgets/cell.dart'; +import '../../widgets/dialog.dart'; +import '../../widgets/toast.dart'; + +class SecurityPage extends StatelessWidget { + const SecurityPage({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + backgroundColor: context.theme.background, + appBar: MixinAppBar( + title: Text(context.l10n.security), + ), + body: ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: const SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 40), + _Passcode(), + ], + ), + ), + ), + ); +} + +class _Passcode extends HookWidget { + const _Passcode(); + + @override + Widget build(BuildContext context) { + final hasPasscode = + useStream(SecurityKeyValue.instance.watchHasPasscode()).data ?? false; + + return CellGroup( + child: CellItem( + title: Text(context.l10n.screenPasscode), + trailing: Transform.scale( + scale: 0.7, + child: CupertinoSwitch( + activeColor: context.theme.accent, + value: hasPasscode, + onChanged: (value) { + if (!value) { + SecurityKeyValue.instance.passcode = null; + return; + } + showMixinDialog( + context: context, + child: const _InputPasscode(), + ); + }, + ), + ), + ), + ); + } +} + +class _InputPasscode extends HookWidget { + const _InputPasscode(); + + @override + Widget build(BuildContext context) { + final focusNode = useFocusNode(); + useEffect(() { + focusNode.requestFocus(); + void listener() { + if (focusNode.hasFocus) return; + focusNode.requestFocus(); + } + + focusNode.addListener(listener); + return () { + focusNode.removeListener(listener); + }; + }, []); + + final textEditingController = useTextEditingController(); + + final passcode = useState(null); + final confirmPasscode = useState(null); + + useEffect(() { + if (passcode.value == null) return; + if (confirmPasscode.value == null) return; + + if (passcode.value != confirmPasscode.value) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showToastFailed(context.l10n.passcodeIncorrect); + }); + + passcode.value = null; + confirmPasscode.value = null; + textEditingController.text = ''; + return; + } + + SecurityKeyValue.instance.passcode = passcode.value; + Navigator.maybePop(context); + }); + + return Padding( + padding: const EdgeInsets.only(top: 20, bottom: 80), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + children: [ + Spacer(), + Padding( + padding: EdgeInsets.only(right: 12, top: 12), + child: MixinCloseButton(), + ), + ], + ), + Text( + passcode.value != null + ? context.l10n.confirmPasscodeDesc + : context.l10n.setPasscodeDesc, + textAlign: TextAlign.center, + style: TextStyle( + color: context.theme.text, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 40), + SizedBox( + width: 215, + child: PinCodeTextField( + appContext: context, + length: 6, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + autoFocus: true, + keyboardType: TextInputType.number, + useHapticFeedback: true, + pinTheme: PinTheme( + activeColor: context.theme.text, + inactiveColor: context.theme.text, + selectedColor: context.theme.text, + fieldWidth: 15, + borderWidth: 2, + ), + textStyle: TextStyle( + fontSize: 18, + color: context.theme.text, + ), + autoDisposeControllers: false, + focusNode: focusNode, + controller: textEditingController, + showCursor: false, + onCompleted: (value) { + if (passcode.value != null) { + confirmPasscode.value = value; + } else { + passcode.value = value; + textEditingController.text = ''; + } + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/setting/setting_page.dart b/lib/ui/setting/setting_page.dart index b4b2858a05..c6e8f94ad6 100644 --- a/lib/ui/setting/setting_page.dart +++ b/lib/ui/setting/setting_page.dart @@ -114,13 +114,14 @@ class SettingPage extends HookWidget { title: context.l10n.dataAndStorageUsage, ), _Item( + leadingAssetName: Resources.assetsImagesShieldSvg, + pageName: ResponsiveNavigatorCubit.securityPage, + title: context.l10n.security, + ), + _Item( + leadingAssetName: Resources.assetsImagesProxySvg, pageName: ResponsiveNavigatorCubit.proxyPage, title: context.l10n.proxy, - leading: Icon( - Icons.shield_outlined, - size: 24, - color: context.theme.icon, - ), ), _Item( leadingAssetName: @@ -170,6 +171,7 @@ class _Item extends StatelessWidget { this.color, this.onTap, this.trailing = const Arrow(), + // ignore: unused_element this.leading, }); diff --git a/lib/utils/file.dart b/lib/utils/file.dart index fcd2c710b8..630b434e2b 100644 --- a/lib/utils/file.dart +++ b/lib/utils/file.dart @@ -45,7 +45,7 @@ Future saveFileToSystem( d('saveFileToSystem: $file, $targetName, $mineType, $extension'); - var path = await file_selector.getSavePath( + var path = (await file_selector.getSaveLocation( confirmButtonText: context.l10n.save, suggestedName: targetName, acceptedTypeGroups: [ @@ -55,7 +55,8 @@ Future saveFileToSystem( mimeTypes: [if (mineType != null) mineType], ), ], - ); + )) + ?.path; if (path == null || path.isEmpty) { return false; } diff --git a/lib/utils/hive_key_values.dart b/lib/utils/hive_key_values.dart index 453db09d1d..5d44e2eec3 100644 --- a/lib/utils/hive_key_values.dart +++ b/lib/utils/hive_key_values.dart @@ -7,6 +7,7 @@ import 'package:path/path.dart' as p; import '../account/account_key_value.dart'; import '../account/scam_warning_key_value.dart'; +import '../account/security_key_value.dart'; import '../account/session_key_value.dart'; import '../account/show_pin_message_key_value.dart'; import '../crypto/crypto_key_value.dart'; @@ -22,6 +23,7 @@ Future initKeyValues(String identityNumber) => Future.wait([ ScamWarningKeyValue.instance.init(identityNumber), DownloadKeyValue.instance.init(identityNumber), SessionKeyValue.instance.init(identityNumber), + SecurityKeyValue.instance.init(identityNumber), ]); Future clearKeyValues() => Future.wait([ @@ -32,6 +34,7 @@ Future clearKeyValues() => Future.wait([ ScamWarningKeyValue.instance.delete(), DownloadKeyValue.instance.delete(), SessionKeyValue.instance.delete(), + SecurityKeyValue.instance.delete(), ]); abstract class HiveKeyValue { diff --git a/lib/widgets/auth.dart b/lib/widgets/auth.dart new file mode 100644 index 0000000000..5bafca7387 --- /dev/null +++ b/lib/widgets/auth.dart @@ -0,0 +1,235 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:pin_code_fields/pin_code_fields.dart'; +import 'package:rxdart/rxdart.dart'; + +import '../account/account_server.dart'; +import '../account/security_key_value.dart'; +import '../constants/resources.dart'; +import '../ui/home/bloc/multi_auth_cubit.dart'; +import '../utils/app_lifecycle.dart'; +import '../utils/event_bus.dart'; +import '../utils/extension/extension.dart'; +import '../utils/hook.dart'; + +const lockDuration = Duration(minutes: 1); + +enum LockEvent { lock, unlock } + +class AuthGuard extends HookWidget { + const AuthGuard({ + super.key, + required this.child, + }); + + final Widget child; + + @override + Widget build(BuildContext context) { + final authAvailable = + useBlocState().current != null; + AccountServer? accountServer; + try { + accountServer = context.read(); + } catch (_) {} + final signed = authAvailable && accountServer != null; + + if (signed) return _AuthGuard(child: child); + + return child; + } +} + +class _AuthGuard extends HookWidget { + const _AuthGuard({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final focusNode = useFocusNode(); + final textEditingController = useTextEditingController(); + final hasPasscode = + useMemoizedStream(SecurityKeyValue.instance.watchHasPasscode).data ?? + SecurityKeyValue.instance.hasPasscode; + + final hasError = useState(false); + final lock = useState(SecurityKeyValue.instance.hasPasscode); + + useEffect(() { + final listen = + EventBus.instance.on.whereType().listen((event) { + lock.value = event == LockEvent.lock; + }); + + return listen.cancel; + }, []); + + useEffect(() { + Timer? timer; + void dispose() { + timer?.cancel(); + timer = null; + } + + void listener() { + if (lock.value) return; + + final needLock = !isAppActive; + + if (needLock) { + timer = Timer(lockDuration, () { + if (!hasPasscode) { + lock.value = false; + return; + } + + lock.value = !isAppActive; + }); + } else { + dispose(); + lock.value = needLock; + } + } + + listener(); + appActiveListener.addListener(listener); + return () { + dispose(); + appActiveListener.removeListener(listener); + }; + }, [hasPasscode]); + + useEffect(() { + focusNode.requestFocus(); + void listener() { + if (focusNode.hasFocus) return; + focusNode.requestFocus(); + } + + bool handler(KeyEvent _) { + listener(); + return false; + } + + FocusManager.instance.addListener(listener); + appActiveListener.addListener(listener); + ServicesBinding.instance.keyboard.addHandler(handler); + return () { + appActiveListener.removeListener(listener); + FocusManager.instance.removeListener(listener); + ServicesBinding.instance.keyboard.removeHandler(handler); + }; + }, [lock.value]); + + return Stack( + children: [ + child, + if (lock.value) + GestureDetector( + onTap: focusNode.requestFocus, + behavior: HitTestBehavior.translucent, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 20, + sigmaY: 20, + ), + child: MaterialApp( + color: Colors.transparent, + home: Material( + color: Colors.transparent, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Resources.assetsImagesLockSvg, + width: 68, + height: 68, + colorFilter: ColorFilter.mode( + context.theme.icon, BlendMode.srcIn), + ), + const SizedBox(height: 24), + Text( + context.l10n.unlockWithWasscode, + textAlign: TextAlign.center, + style: TextStyle( + color: context.theme.text, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 40), + SizedBox( + width: 204, + // height: 14, + child: PinCodeTextField( + appContext: context, + length: 6, + controller: textEditingController, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly + ], + pinTheme: PinTheme( + activeColor: context.theme.text, + inactiveColor: context.theme.text, + selectedColor: context.theme.text, + fieldWidth: 15, + borderWidth: 1, + shape: PinCodeFieldShape.circle, + ), + obscureText: true, + autoDisposeControllers: false, + obscuringWidget: Container( + decoration: BoxDecoration( + color: context.theme.text, + shape: BoxShape.circle, + )), + autoFocus: true, + focusNode: focusNode, + showCursor: false, + onCompleted: (value) { + textEditingController.text = ''; + if (SecurityKeyValue.instance.passcode == value) { + lock.value = false; + } else { + hasError.value = true; + } + }, + onChanged: (value) { + hasError.value = false; + }, + ), + ), + const SizedBox(height: 28), + Visibility( + visible: hasError.value, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: Text( + context.l10n.passcodeIncorrect, + textAlign: TextAlign.center, + style: TextStyle( + color: context.theme.red, + fontSize: 16, + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ) + ], + ); + } +} diff --git a/lib/widgets/dialog.dart b/lib/widgets/dialog.dart index 90071e7c6b..2ebc9d490b 100644 --- a/lib/widgets/dialog.dart +++ b/lib/widgets/dialog.dart @@ -164,9 +164,6 @@ class _DialogPage extends StatelessWidget { child: DecoratedBox( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(11)), - border: Border.all( - color: const Color.fromRGBO(255, 255, 255, 0.08), - ), boxShadow: [ const BoxShadow( color: Color.fromRGBO(0, 0, 0, 0.15), @@ -179,9 +176,9 @@ class _DialogPage extends StatelessWidget { blurRadius: lerpDouble(16, 6, context.brightnessValue)!, ), ], - color: backgroundColor ?? context.theme.popUp, ), - child: ClipRRect( + child: Material( + color: backgroundColor ?? context.theme.popUp, borderRadius: const BorderRadius.all(Radius.circular(11)), child: child, ), diff --git a/lib/widgets/toast.dart b/lib/widgets/toast.dart index 37aa712f5d..bb0dc05176 100644 --- a/lib/widgets/toast.dart +++ b/lib/widgets/toast.dart @@ -23,9 +23,11 @@ class Toast { static void createView({ required WidgetBuilder builder, Duration? duration = Toast.shortDuration, + BuildContext? context, }) { dismiss(); _entry = showOverlay( + context: context, (context, progress) => Opacity( opacity: progress, child: builder(context), @@ -138,7 +140,9 @@ class ToastError extends Error { final String Function(BuildContext)? messageBuilder; } -void showToastFailed(Object? error) => Toast.createView( +void showToastFailed(Object? error, {BuildContext? context}) => + Toast.createView( + context: context, builder: (context) => ToastWidget( barrierColor: Colors.transparent, icon: const _Failed(), diff --git a/lib/widgets/window/menus.dart b/lib/widgets/window/menus.dart index a09c1bb85e..6de5e8170f 100644 --- a/lib/widgets/window/menus.dart +++ b/lib/widgets/window/menus.dart @@ -9,14 +9,17 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:window_manager/window_manager.dart'; import '../../account/account_server.dart'; +import '../../account/security_key_value.dart'; import '../../ui/home/bloc/multi_auth_cubit.dart'; import '../../ui/home/bloc/slide_category_cubit.dart'; import '../../ui/home/conversation/conversation_hotkey.dart'; import '../../utils/device_transfer/device_transfer_dialog.dart'; +import '../../utils/event_bus.dart'; import '../../utils/extension/extension.dart'; import '../../utils/hook.dart'; import '../../utils/uri_utils.dart'; import '../actions/actions.dart'; +import '../auth.dart'; abstract class ConversationMenuHandle { Stream get isMuted; @@ -115,6 +118,12 @@ class _Menus extends HookWidget { ).data ?? false; + final hasPasscode = useMemoizedStream(signed + ? SecurityKeyValue.instance.watchHasPasscode + : () => Stream.value(false)) + .data ?? + false; + PlatformMenu buildConversationMenu() => PlatformMenu( label: context.l10n.conversation, menus: [ @@ -182,6 +191,19 @@ class _Menus extends HookWidget { : null, ), ]), + PlatformMenuItemGroup(members: [ + PlatformMenuItem( + label: context.l10n.lock, + shortcut: const SingleActivator( + LogicalKeyboardKey.keyL, + meta: true, + shift: true, + ), + onSelected: hasPasscode + ? () => EventBus.instance.fire(LockEvent.lock) + : null, + ), + ]), PlatformMenuItemGroup( members: [ PlatformMenuItem( diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index a69d1b1a3c..4ab0e96d84 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -209,7 +209,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 4a9fe9b8c7..07dcc4480a 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@