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 @@