Implement AppLockScreen as per the designs. (#1925)
Fix a bug in the unlock flow
This commit is contained in:
@@ -351,6 +351,7 @@
|
||||
64AB99285DC4437C0DDE9585 /* MenuSheetLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49ABAB186CF00B15C5521D04 /* MenuSheetLabelStyle.swift */; };
|
||||
64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */; };
|
||||
64D05250CEDE8B604119F6E6 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981663D961C94270FA035FD0 /* Alert.swift */; };
|
||||
64E541F88F35BD126C4AFCA1 /* AppLockScreenPINKeypad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38345442415E07A931197C55 /* AppLockScreenPINKeypad.swift */; };
|
||||
64F43D7390DA2A0AFD6BA911 /* VideoRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */; };
|
||||
64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */; };
|
||||
651341E67C3514F9811A1EC1 /* LoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F598B1B346DAF223651C91 /* LoginScreenCoordinator.swift */; };
|
||||
@@ -1174,6 +1175,7 @@
|
||||
37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringTests.swift; sourceTree = "<group>"; };
|
||||
37E727F7E0BCE8A0BBFD33FF /* OnboardingScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyMock.swift; sourceTree = "<group>"; };
|
||||
38345442415E07A931197C55 /* AppLockScreenPINKeypad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenPINKeypad.swift; sourceTree = "<group>"; };
|
||||
38E521D6C2BF8DF0DFB35146 /* DeveloperOptionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreen.swift; sourceTree = "<group>"; };
|
||||
3948D16F021DFDB2CD26EAA8 /* MockBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBackgroundTaskService.swift; sourceTree = "<group>"; };
|
||||
398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadableFrameModifier.swift; sourceTree = "<group>"; };
|
||||
@@ -4400,6 +4402,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
56D6F88FE35A0979D2821E06 /* AppLockScreen.swift */,
|
||||
38345442415E07A931197C55 /* AppLockScreenPINKeypad.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
@@ -5091,6 +5094,7 @@
|
||||
06F8EDF52E33A2D36BCC1161 /* AppLockScreen.swift in Sources */,
|
||||
9912F9EB2D6589141A2957B4 /* AppLockScreenCoordinator.swift in Sources */,
|
||||
2DD9D0FE7CB5CFC80D071451 /* AppLockScreenModels.swift in Sources */,
|
||||
64E541F88F35BD126C4AFCA1 /* AppLockScreenPINKeypad.swift in Sources */,
|
||||
97969EF0B9C412CD38E5CA93 /* AppLockScreenViewModel.swift in Sources */,
|
||||
E79D79CDAFE8BEBCC3AECA54 /* AppLockScreenViewModelProtocol.swift in Sources */,
|
||||
1D623953F970D11F6F38499C /* AppLockService.swift in Sources */,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"a11y_notifications_mentions_only" = "Mentions only";
|
||||
"a11y_notifications_muted" = "Muted";
|
||||
"a11y_pause" = "Pause";
|
||||
"a11y_pin_field" = "PIN field";
|
||||
"a11y_play" = "Play";
|
||||
"a11y_poll" = "Poll";
|
||||
"a11y_poll_end" = "Ended poll";
|
||||
@@ -95,6 +96,7 @@
|
||||
"common_editing" = "Editing";
|
||||
"common_emote" = "* %1$@ %2$@";
|
||||
"common_encryption_enabled" = "Encryption enabled";
|
||||
"common_enter_your_pin" = "Enter your PIN";
|
||||
"common_error" = "Error";
|
||||
"common_everyone" = "Everyone";
|
||||
"common_file" = "File";
|
||||
@@ -130,6 +132,7 @@
|
||||
"common_rich_text_editor" = "Rich text editor";
|
||||
"common_room_name" = "Room name";
|
||||
"common_room_name_placeholder" = "e.g. your project name";
|
||||
"common_screen_lock" = "Screen lock";
|
||||
"common_search_for_someone" = "Search for someone";
|
||||
"common_search_results" = "Search results";
|
||||
"common_security" = "Security";
|
||||
@@ -151,6 +154,7 @@
|
||||
"common_unable_to_decrypt" = "Unable to decrypt";
|
||||
"common_unable_to_invite_message" = "Invites couldn't be sent to one or more users.";
|
||||
"common_unable_to_invite_title" = "Unable to send invite(s)";
|
||||
"common_unlock" = "Unlock";
|
||||
"common_unmute" = "Unmute";
|
||||
"common_unsupported_event" = "Unsupported event";
|
||||
"common_username" = "Username";
|
||||
@@ -260,6 +264,18 @@
|
||||
"screen_analytics_prompt_third_party_sharing" = "We won't share your data with third parties";
|
||||
"screen_analytics_prompt_title" = "Help improve %1$@";
|
||||
"screen_analytics_settings_share_data" = "Share analytics data";
|
||||
"screen_app_lock_forgot_pin" = "Forgot PIN?";
|
||||
"screen_app_lock_settings_change_pin" = "Change PIN code";
|
||||
"screen_app_lock_settings_enable_biometric_unlock" = "Allow biometric unlock";
|
||||
"screen_app_lock_settings_enable_face_id_ios" = "Allow Face ID";
|
||||
"screen_app_lock_settings_enable_optic_id_ios" = "Allow Optic ID";
|
||||
"screen_app_lock_settings_enable_touch_id_ios" = "Allow Touch ID";
|
||||
"screen_app_lock_settings_remove_pin" = "Remove PIN";
|
||||
"screen_app_lock_settings_remove_pin_alert_message" = "Are you sure you want to remove PIN?";
|
||||
"screen_app_lock_settings_remove_pin_alert_title" = "Remove PIN?";
|
||||
"screen_app_lock_signout_alert_message" = "You’ll need to re-login and create a new PIN to proceed";
|
||||
"screen_app_lock_signout_alert_title" = "You are being signed out";
|
||||
"screen_app_lock_subtitle" = "You have 3 attempts to unlock";
|
||||
"screen_bug_report_attach_screenshot" = "Attach screenshot";
|
||||
"screen_bug_report_contact_me" = "You may contact me if you have any follow up questions.";
|
||||
"screen_bug_report_contact_me_title" = "Contact me";
|
||||
|
||||
@@ -2,6 +2,22 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>a11y_digits_entered</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%1$d digit entered</string>
|
||||
<key>other</key>
|
||||
<string>%1$d digits entered</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>common_member_count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
@@ -146,6 +162,22 @@
|
||||
<string>%1$d room changes</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>screen_app_lock_subtitle_wrong_pin</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Wrong PIN. You have %1$d more chance</string>
|
||||
<key>other</key>
|
||||
<string>Wrong PIN. You have %1$d more chances</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>screen_room_member_list_header_title</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
|
||||
@@ -4,18 +4,6 @@
|
||||
/* Used for testing */
|
||||
"untranslated" = "Untranslated";
|
||||
|
||||
"screen_app_lock_title" = "%@ is locked";
|
||||
"screen_app_lock_settings_change_pin" = "Change PIN code";
|
||||
"screen_app_lock_settings_remove_pin" = "Remove PIN";
|
||||
"screen_app_lock_settings_enable_touch_id_ios" = "Allow Touch ID";
|
||||
"screen_app_lock_settings_enable_face_id_ios" = "Allow Face ID";
|
||||
"screen_app_lock_settings_enable_optic_id_ios" = "Allow Optic ID";
|
||||
"screen_app_lock_settings_enable_biometric_unlock" = "Allow biometric unlock";
|
||||
"screen_app_lock_settings_remove_pin_alert_title" = "Remove PIN?";
|
||||
"screen_app_lock_settings_remove_pin_alert_message" = "Are you sure you want to remove PIN?";
|
||||
"common_unlock" = "Unlock";
|
||||
"common_screen_lock" = "Screen lock";
|
||||
|
||||
// MARK: - Soft logout
|
||||
|
||||
"soft_logout_signin_title" = "Sign in";
|
||||
|
||||
@@ -51,10 +51,6 @@ struct Application: App {
|
||||
openURLInSystemBrowser($0)
|
||||
}
|
||||
}
|
||||
.introspect(.window, on: .supportedVersions) { window in
|
||||
// Workaround for SwiftUI not consistently applying the tint colour to Alerts/Confirmation Dialogs.
|
||||
window.tintColor = .compound.textActionPrimary
|
||||
}
|
||||
.task {
|
||||
appCoordinator.start()
|
||||
}
|
||||
|
||||
@@ -38,8 +38,10 @@ class WindowManager {
|
||||
/// Configures the window manager to operate on the supplied scene.
|
||||
func configure(with windowScene: UIWindowScene) {
|
||||
mainWindow = windowScene.keyWindow
|
||||
mainWindow.tintColor = .compound.textActionPrimary
|
||||
|
||||
overlayWindow = UIWindow(windowScene: windowScene)
|
||||
overlayWindow.tintColor = .compound.textActionPrimary
|
||||
overlayWindow.backgroundColor = .clear
|
||||
// We don't support user interaction on our indicators so disable interaction, to pass
|
||||
// touches through to the main window. If this changes, there's another solution here:
|
||||
@@ -47,6 +49,7 @@ class WindowManager {
|
||||
overlayWindow.isUserInteractionEnabled = false
|
||||
|
||||
alternateWindow = UIWindow(windowScene: windowScene)
|
||||
alternateWindow.tintColor = .compound.textActionPrimary
|
||||
|
||||
delegate?.windowManagerDidConfigureWindows(self)
|
||||
}
|
||||
|
||||
@@ -67,8 +67,14 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func applicationWillEnterForeground() {
|
||||
guard appLockService.isEnabled, appLockService.computeNeedsUnlock(willEnterForegroundAt: .now) else { return }
|
||||
showUnlockScreen()
|
||||
guard appLockService.isEnabled else { return }
|
||||
|
||||
if appLockService.computeNeedsUnlock(willEnterForegroundAt: .now) {
|
||||
showUnlockScreen()
|
||||
} else {
|
||||
// Reveal the app again if within the grace period.
|
||||
actionsSubject.send(.unlockApp)
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays the unlock flow with the app's placeholder view to hide obscure the view hierarchy in the app switcher.
|
||||
|
||||
@@ -10,30 +10,6 @@ import Foundation
|
||||
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
||||
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||
public enum UntranslatedL10n {
|
||||
/// Screen lock
|
||||
public static var commonScreenLock: String { return UntranslatedL10n.tr("Untranslated", "common_screen_lock") }
|
||||
/// Unlock
|
||||
public static var commonUnlock: String { return UntranslatedL10n.tr("Untranslated", "common_unlock") }
|
||||
/// Change PIN code
|
||||
public static var screenAppLockSettingsChangePin: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_change_pin") }
|
||||
/// Allow biometric unlock
|
||||
public static var screenAppLockSettingsEnableBiometricUnlock: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_enable_biometric_unlock") }
|
||||
/// Allow Face ID
|
||||
public static var screenAppLockSettingsEnableFaceIdIos: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_enable_face_id_ios") }
|
||||
/// Allow Optic ID
|
||||
public static var screenAppLockSettingsEnableOpticIdIos: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_enable_optic_id_ios") }
|
||||
/// Allow Touch ID
|
||||
public static var screenAppLockSettingsEnableTouchIdIos: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_enable_touch_id_ios") }
|
||||
/// Remove PIN
|
||||
public static var screenAppLockSettingsRemovePin: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_remove_pin") }
|
||||
/// Are you sure you want to remove PIN?
|
||||
public static var screenAppLockSettingsRemovePinAlertMessage: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_remove_pin_alert_message") }
|
||||
/// Remove PIN?
|
||||
public static var screenAppLockSettingsRemovePinAlertTitle: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_remove_pin_alert_title") }
|
||||
/// %@ is locked
|
||||
public static func screenAppLockTitle(_ p1: Any) -> String {
|
||||
return UntranslatedL10n.tr("Untranslated", "screen_app_lock_title", String(describing: p1))
|
||||
}
|
||||
/// Clear all data currently stored on this device?
|
||||
/// Sign in again to access your account data and messages.
|
||||
public static var softLogoutClearDataDialogContent: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_content") }
|
||||
|
||||
@@ -12,6 +12,10 @@ import Foundation
|
||||
public enum L10n {
|
||||
/// Delete
|
||||
public static var a11yDelete: String { return L10n.tr("Localizable", "a11y_delete") }
|
||||
/// Plural format key: "%#@COUNT@"
|
||||
public static func a11yDigitsEntered(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "a11y_digits_entered", p1)
|
||||
}
|
||||
/// Hide password
|
||||
public static var a11yHidePassword: String { return L10n.tr("Localizable", "a11y_hide_password") }
|
||||
/// Mentions only
|
||||
@@ -20,6 +24,8 @@ public enum L10n {
|
||||
public static var a11yNotificationsMuted: String { return L10n.tr("Localizable", "a11y_notifications_muted") }
|
||||
/// Pause
|
||||
public static var a11yPause: String { return L10n.tr("Localizable", "a11y_pause") }
|
||||
/// PIN field
|
||||
public static var a11yPinField: String { return L10n.tr("Localizable", "a11y_pin_field") }
|
||||
/// Play
|
||||
public static var a11yPlay: String { return L10n.tr("Localizable", "a11y_play") }
|
||||
/// Poll
|
||||
@@ -206,6 +212,8 @@ public enum L10n {
|
||||
}
|
||||
/// Encryption enabled
|
||||
public static var commonEncryptionEnabled: String { return L10n.tr("Localizable", "common_encryption_enabled") }
|
||||
/// Enter your PIN
|
||||
public static var commonEnterYourPin: String { return L10n.tr("Localizable", "common_enter_your_pin") }
|
||||
/// Error
|
||||
public static var commonError: String { return L10n.tr("Localizable", "common_error") }
|
||||
/// Everyone
|
||||
@@ -296,6 +304,8 @@ public enum L10n {
|
||||
public static var commonRoomName: String { return L10n.tr("Localizable", "common_room_name") }
|
||||
/// e.g. your project name
|
||||
public static var commonRoomNamePlaceholder: String { return L10n.tr("Localizable", "common_room_name_placeholder") }
|
||||
/// Screen lock
|
||||
public static var commonScreenLock: String { return L10n.tr("Localizable", "common_screen_lock") }
|
||||
/// Search for someone
|
||||
public static var commonSearchForSomeone: String { return L10n.tr("Localizable", "common_search_for_someone") }
|
||||
/// Search results
|
||||
@@ -338,6 +348,8 @@ public enum L10n {
|
||||
public static var commonUnableToInviteMessage: String { return L10n.tr("Localizable", "common_unable_to_invite_message") }
|
||||
/// Unable to send invite(s)
|
||||
public static var commonUnableToInviteTitle: String { return L10n.tr("Localizable", "common_unable_to_invite_title") }
|
||||
/// Unlock
|
||||
public static var commonUnlock: String { return L10n.tr("Localizable", "common_unlock") }
|
||||
/// Unmute
|
||||
public static var commonUnmute: String { return L10n.tr("Localizable", "common_unmute") }
|
||||
/// Unsupported event
|
||||
@@ -634,6 +646,34 @@ public enum L10n {
|
||||
public static var screenAnalyticsSettingsReadTermsContentLink: String { return L10n.tr("Localizable", "screen_analytics_settings_read_terms_content_link") }
|
||||
/// Share analytics data
|
||||
public static var screenAnalyticsSettingsShareData: String { return L10n.tr("Localizable", "screen_analytics_settings_share_data") }
|
||||
/// Forgot PIN?
|
||||
public static var screenAppLockForgotPin: String { return L10n.tr("Localizable", "screen_app_lock_forgot_pin") }
|
||||
/// Change PIN code
|
||||
public static var screenAppLockSettingsChangePin: String { return L10n.tr("Localizable", "screen_app_lock_settings_change_pin") }
|
||||
/// Allow biometric unlock
|
||||
public static var screenAppLockSettingsEnableBiometricUnlock: String { return L10n.tr("Localizable", "screen_app_lock_settings_enable_biometric_unlock") }
|
||||
/// Allow Face ID
|
||||
public static var screenAppLockSettingsEnableFaceIdIos: String { return L10n.tr("Localizable", "screen_app_lock_settings_enable_face_id_ios") }
|
||||
/// Allow Optic ID
|
||||
public static var screenAppLockSettingsEnableOpticIdIos: String { return L10n.tr("Localizable", "screen_app_lock_settings_enable_optic_id_ios") }
|
||||
/// Allow Touch ID
|
||||
public static var screenAppLockSettingsEnableTouchIdIos: String { return L10n.tr("Localizable", "screen_app_lock_settings_enable_touch_id_ios") }
|
||||
/// Remove PIN
|
||||
public static var screenAppLockSettingsRemovePin: String { return L10n.tr("Localizable", "screen_app_lock_settings_remove_pin") }
|
||||
/// Are you sure you want to remove PIN?
|
||||
public static var screenAppLockSettingsRemovePinAlertMessage: String { return L10n.tr("Localizable", "screen_app_lock_settings_remove_pin_alert_message") }
|
||||
/// Remove PIN?
|
||||
public static var screenAppLockSettingsRemovePinAlertTitle: String { return L10n.tr("Localizable", "screen_app_lock_settings_remove_pin_alert_title") }
|
||||
/// You’ll need to re-login and create a new PIN to proceed
|
||||
public static var screenAppLockSignoutAlertMessage: String { return L10n.tr("Localizable", "screen_app_lock_signout_alert_message") }
|
||||
/// You are being signed out
|
||||
public static var screenAppLockSignoutAlertTitle: String { return L10n.tr("Localizable", "screen_app_lock_signout_alert_title") }
|
||||
/// You have 3 attempts to unlock
|
||||
public static var screenAppLockSubtitle: String { return L10n.tr("Localizable", "screen_app_lock_subtitle") }
|
||||
/// Plural format key: "%#@COUNT@"
|
||||
public static func screenAppLockSubtitleWrongPin(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "screen_app_lock_subtitle_wrong_pin", p1)
|
||||
}
|
||||
/// Attach screenshot
|
||||
public static var screenBugReportAttachScreenshot: String { return L10n.tr("Localizable", "screen_bug_report_attach_screenshot") }
|
||||
/// You may contact me if you have any follow up questions.
|
||||
|
||||
@@ -173,6 +173,7 @@ struct A11yIdentifiers {
|
||||
let secureBackup = "settings-secure_backup"
|
||||
let notifications = "settings-notifications"
|
||||
let analytics = "settings-analytics"
|
||||
let screenLock = "settings-screen_lock"
|
||||
let reportBug = "settings-report_bug"
|
||||
let about = "settings_about"
|
||||
let advancedSettings = "settings_advanced-settings"
|
||||
|
||||
@@ -22,19 +22,45 @@ enum AppLockScreenViewModelAction {
|
||||
}
|
||||
|
||||
struct AppLockScreenViewState: BindableState {
|
||||
var bindings: AppLockScreenViewStateBindings
|
||||
}
|
||||
|
||||
struct AppLockScreenViewStateBindings { }
|
||||
|
||||
enum AppLockScreenViewAction: CustomStringConvertible {
|
||||
/// Attempt to unlock the app with the supplied PIN code.
|
||||
case submitPINCode(String)
|
||||
private let maximumAttempts = 3
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .submitPINCode:
|
||||
return "submitPINCode"
|
||||
/// The number of times the user attempted to enter their PIN.
|
||||
var numberOfPINAttempts = 0
|
||||
|
||||
var bindings: AppLockScreenViewStateBindings
|
||||
|
||||
/// The number of digits the user has entered so far.
|
||||
var numberOfDigitsEntered: Int { bindings.pinCode.count }
|
||||
/// Whether the subtitle is in a warning state or not.
|
||||
var isSubtitleWarning: Bool { numberOfPINAttempts > 0 }
|
||||
/// The string shown in the screen's subtitle.
|
||||
var subtitle: String {
|
||||
if !isSubtitleWarning {
|
||||
return L10n.screenAppLockSubtitle
|
||||
} else {
|
||||
return L10n.screenAppLockSubtitleWrongPin(maximumAttempts - numberOfPINAttempts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AppLockScreenViewStateBindings {
|
||||
/// The PIN code entered by the user.
|
||||
var pinCode = ""
|
||||
var alertInfo: AlertInfo<AppLockScreenAlertType>?
|
||||
}
|
||||
|
||||
enum AppLockScreenAlertType {
|
||||
/// The user has failed too many times, they're being signed out.
|
||||
case forceSignOut
|
||||
/// The user has forgotten their PIN, confirm they're happy to sign out.
|
||||
case confirmResetPIN
|
||||
}
|
||||
|
||||
enum AppLockScreenViewAction {
|
||||
/// Attempt to unlock the app with the supplied PIN code.
|
||||
case submitPINCode
|
||||
/// Clears the PIN code after a failure animation.
|
||||
case clearPINCode
|
||||
/// The user didn't heed the warnings and can't remember their PIN.
|
||||
case forgotPIN
|
||||
}
|
||||
|
||||
@@ -39,13 +39,43 @@ class AppLockScreenViewModel: AppLockScreenViewModelType, AppLockScreenViewModel
|
||||
MXLog.info("View model: received view action: \(viewAction)")
|
||||
|
||||
switch viewAction {
|
||||
case .submitPINCode(let pinCode):
|
||||
guard appLockService.unlock(with: pinCode) else {
|
||||
MXLog.warning("Invalid PIN code entered.")
|
||||
// Indicate failure here.
|
||||
case .submitPINCode:
|
||||
guard appLockService.unlock(with: state.bindings.pinCode) else {
|
||||
handleInvalidPIN()
|
||||
return
|
||||
}
|
||||
actionsSubject.send(.appUnlocked)
|
||||
case .clearPINCode:
|
||||
state.bindings.pinCode = ""
|
||||
case .forgotPIN:
|
||||
handleForgotPIN()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func handleForgotPIN() {
|
||||
state.bindings.alertInfo = .init(id: .confirmResetPIN,
|
||||
title: L10n.screenAppLockSignoutAlertTitle,
|
||||
message: L10n.screenAppLockSignoutAlertMessage,
|
||||
primaryButton: .init(title: L10n.actionOk, action: forceSignOut),
|
||||
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||
}
|
||||
|
||||
private func handleInvalidPIN() {
|
||||
MXLog.warning("Invalid PIN code entered.")
|
||||
state.numberOfPINAttempts += 1
|
||||
|
||||
if state.numberOfPINAttempts == 3 {
|
||||
state.bindings.alertInfo = .init(id: .forceSignOut,
|
||||
title: L10n.screenAppLockSignoutAlertTitle,
|
||||
message: L10n.screenAppLockSignoutAlertMessage,
|
||||
primaryButton: .init(title: L10n.actionOk, action: nil))
|
||||
forceSignOut()
|
||||
}
|
||||
}
|
||||
|
||||
private func forceSignOut() {
|
||||
// To be implemented.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,36 +17,107 @@
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
// This implementation is only for development purposes.
|
||||
|
||||
struct AppLockScreen: View {
|
||||
@ObservedObject var context: AppLockScreenViewModel.Context
|
||||
|
||||
/// The size of each dot within the PIN input field.
|
||||
@ScaledMetric private var pinDotSize = 14
|
||||
/// Used to animate the PIN input field on failure.
|
||||
@State private var pinInputFieldOffset = 0.0
|
||||
|
||||
/// A focus state to highlight a failed PIN entry in VoiceOver.
|
||||
@AccessibilityFocusState private var accessibilitySubtitleFocus: Bool
|
||||
|
||||
var subtitleColor: Color {
|
||||
context.viewState.isSubtitleWarning ? .compound.textCriticalPrimary : .compound.textPrimary
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog {
|
||||
VStack(spacing: 8) {
|
||||
HeroImage(image: Image(systemSymbol: .lock))
|
||||
.symbolVariant(.fill)
|
||||
.padding(.bottom, 8)
|
||||
VStack(spacing: 32) {
|
||||
header
|
||||
|
||||
Text(UntranslatedL10n.screenAppLockTitle(InfoPlistReader.main.bundleDisplayName))
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
pinInputField
|
||||
.padding(.bottom, 16)
|
||||
.offset(x: pinInputFieldOffset)
|
||||
.onChange(of: context.viewState.numberOfPINAttempts) { newValue in
|
||||
guard newValue > 0 else { return } // Reset without animation in Previews.
|
||||
accessibilitySubtitleFocus = true
|
||||
Task { await animatePINFailure() }
|
||||
}
|
||||
.accessibilityLabel(L10n.a11yPinField)
|
||||
.accessibilityValue(L10n.a11yDigitsEntered(context.viewState.numberOfDigitsEntered))
|
||||
|
||||
AppLockScreenPINKeypad(pinCode: $context.pinCode)
|
||||
.onChange(of: context.pinCode) { newValue in
|
||||
guard newValue.count == 4 else { return }
|
||||
context.send(viewAction: .submitPINCode)
|
||||
}
|
||||
}
|
||||
} bottomContent: {
|
||||
Button(UntranslatedL10n.commonUnlock) {
|
||||
context.send(viewAction: .submitPINCode("0000"))
|
||||
Button(L10n.screenAppLockForgotPin) {
|
||||
context.send(viewAction: .forgotPIN)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.font(.compound.bodyMDSemibold)
|
||||
}
|
||||
.alert(item: $context.alertInfo)
|
||||
}
|
||||
|
||||
var header: some View {
|
||||
VStack(spacing: 8) {
|
||||
CompoundIcon(\.lock, size: .medium, relativeTo: .compound.headingMDBold)
|
||||
.padding(.bottom, 8)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(L10n.commonEnterYourPin)
|
||||
.font(.compound.headingMDBold)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(context.viewState.subtitle)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(subtitleColor)
|
||||
.multilineTextAlignment(.center)
|
||||
.accessibilityFocused($accessibilitySubtitleFocus)
|
||||
}
|
||||
}
|
||||
|
||||
/// The row of dots showing how many digits have been entered.
|
||||
var pinInputField: some View {
|
||||
HStack(spacing: 24) {
|
||||
Circle()
|
||||
.fill(context.viewState.numberOfDigitsEntered > 0 ? .compound.iconPrimary : .compound.bgSubtlePrimary)
|
||||
.frame(width: pinDotSize, height: pinDotSize)
|
||||
Circle()
|
||||
.fill(context.viewState.numberOfDigitsEntered > 1 ? .compound.iconPrimary : .compound.bgSubtlePrimary)
|
||||
.frame(width: pinDotSize, height: pinDotSize)
|
||||
Circle()
|
||||
.fill(context.viewState.numberOfDigitsEntered > 2 ? .compound.iconPrimary : .compound.bgSubtlePrimary)
|
||||
.frame(width: pinDotSize, height: pinDotSize)
|
||||
Circle()
|
||||
.fill(context.viewState.numberOfDigitsEntered > 3 ? .compound.iconPrimary : .compound.bgSubtlePrimary)
|
||||
.frame(width: pinDotSize, height: pinDotSize)
|
||||
}
|
||||
}
|
||||
|
||||
func animatePINFailure() async {
|
||||
withAnimation(.spring(response: 0, dampingFraction: 0.7, blendDuration: 0.0)) {
|
||||
pinInputFieldOffset = 15
|
||||
}
|
||||
|
||||
try? await Task.sleep(for: .milliseconds(50))
|
||||
withAnimation(.spring(response: 0.1, dampingFraction: 0.3, blendDuration: 0.1)) {
|
||||
pinInputFieldOffset = 0
|
||||
}
|
||||
|
||||
try? await Task.sleep(for: .milliseconds(100))
|
||||
context.send(viewAction: .clearPINCode)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
// Add TestablePreview conformance once we have designs.
|
||||
struct AppLockScreen_Previews: PreviewProvider {
|
||||
struct AppLockScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = AppLockScreenViewModel(appLockService: AppLockServiceMock.mock())
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// The custom keypad shown on the App Lock screen when biometrics are disabled.
|
||||
struct AppLockScreenPINKeypad: View {
|
||||
@Binding var pinCode: String
|
||||
|
||||
var body: some View {
|
||||
Grid(horizontalSpacing: 24, verticalSpacing: 16) {
|
||||
ForEach(0..<3) { row in
|
||||
GridRow {
|
||||
ForEach(1..<4) { column in
|
||||
let digit = (3 * row) + column
|
||||
Button("\(digit)") { press(digit) }
|
||||
}
|
||||
}
|
||||
}
|
||||
GridRow {
|
||||
Button("") { }.hidden()
|
||||
Button("0") { press(0) }
|
||||
Button(action: pressDelete) {
|
||||
Image(systemSymbol: .deleteBackward)
|
||||
.symbolVariant(.fill)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(.compound.textPrimary, .compound.bgSubtlePrimary)
|
||||
}
|
||||
.buttonStyle(KeypadButtonStyle(isSolid: false))
|
||||
}
|
||||
}
|
||||
.buttonStyle(KeypadButtonStyle())
|
||||
}
|
||||
|
||||
func press(_ digit: Int) {
|
||||
guard pinCode.count < 4 else { return }
|
||||
UIDevice.current.playInputClick()
|
||||
pinCode.append("\(digit)")
|
||||
}
|
||||
|
||||
func pressDelete() {
|
||||
guard !pinCode.isEmpty else { return }
|
||||
withElementAnimation { _ = pinCode.removeLast() }
|
||||
}
|
||||
}
|
||||
|
||||
private struct KeypadButtonStyle: ButtonStyle {
|
||||
var isSolid = true
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
Circle()
|
||||
.fill(isSolid ? .compound.bgSubtlePrimary : .clear)
|
||||
.frame(width: 80, height: 80)
|
||||
.overlay {
|
||||
configuration.label
|
||||
.font(.compound.headingXLBold)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
}
|
||||
.opacity(configuration.isPressed ? 0.3 : 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct AppLockScreenPINKeypad_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
KeypadTestView()
|
||||
}
|
||||
|
||||
struct KeypadTestView: View {
|
||||
@StateObject var model = PreviewModel()
|
||||
class PreviewModel: ObservableObject {
|
||||
@Published var pinCode = ""
|
||||
var output: String { pinCode.isEmpty ? "Enter code" : pinCode }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 32) {
|
||||
Text(model.output)
|
||||
.font(.compound.headingMD)
|
||||
.animation(.noAnimation, value: model.pinCode)
|
||||
AppLockScreenPINKeypad(pinCode: $model.pinCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,14 +33,14 @@ struct AppLockSettingsScreenViewState: BindableState {
|
||||
case .none:
|
||||
return L10n.commonError
|
||||
case .touchID:
|
||||
return UntranslatedL10n.screenAppLockSettingsEnableTouchIdIos
|
||||
return L10n.screenAppLockSettingsEnableTouchIdIos
|
||||
case .faceID:
|
||||
return UntranslatedL10n.screenAppLockSettingsEnableFaceIdIos
|
||||
return L10n.screenAppLockSettingsEnableFaceIdIos
|
||||
// Requires Xcode 15:
|
||||
// case .opticID:
|
||||
// UntranslatedL10n.screenAppLockSettingsEnableOpticIdIos
|
||||
// L10n.screenAppLockSettingsEnableOpticIdIos
|
||||
@unknown default:
|
||||
return UntranslatedL10n.screenAppLockSettingsEnableBiometricUnlock
|
||||
return L10n.screenAppLockSettingsEnableBiometricUnlock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ class AppLockSettingsScreenViewModel: AppLockSettingsScreenViewModelType, AppLoc
|
||||
/// Shows a confirmation alert to the user before removing their PIN code.
|
||||
private func showRemovePINAlert() {
|
||||
state.bindings.alertInfo = .init(id: .confirmRemovePINCode,
|
||||
title: UntranslatedL10n.screenAppLockSettingsRemovePinAlertTitle,
|
||||
message: UntranslatedL10n.screenAppLockSettingsRemovePinAlertMessage,
|
||||
title: L10n.screenAppLockSettingsRemovePinAlertTitle,
|
||||
message: L10n.screenAppLockSettingsRemovePinAlertMessage,
|
||||
primaryButton: .init(title: L10n.actionYes) { self.completeRemovePIN() },
|
||||
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ struct AppLockSettingsScreen: View {
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
ListRow(label: .plain(title: UntranslatedL10n.screenAppLockSettingsChangePin),
|
||||
ListRow(label: .plain(title: L10n.screenAppLockSettingsChangePin),
|
||||
kind: .button { context.send(viewAction: .changePINCode) })
|
||||
ListRow(label: .plain(title: UntranslatedL10n.screenAppLockSettingsRemovePin, role: .destructive),
|
||||
ListRow(label: .plain(title: L10n.screenAppLockSettingsRemovePin, role: .destructive),
|
||||
kind: .button { context.send(viewAction: .disable) })
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ struct AppLockSettingsScreen: View {
|
||||
}
|
||||
}
|
||||
.compoundList()
|
||||
.navigationTitle(UntranslatedL10n.commonScreenLock)
|
||||
.navigationTitle(L10n.commonScreenLock)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.alert(item: $context.alertInfo)
|
||||
}
|
||||
|
||||
@@ -128,12 +128,12 @@ struct SettingsScreen: View {
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics)
|
||||
|
||||
if context.viewState.showAppLockSettings {
|
||||
ListRow(label: .default(title: UntranslatedL10n.commonScreenLock,
|
||||
ListRow(label: .default(title: L10n.commonScreenLock,
|
||||
systemIcon: .lock),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .appLock)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics)
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.screenLock)
|
||||
}
|
||||
|
||||
ListRow(label: .default(title: L10n.commonReportABug,
|
||||
|
||||
@@ -44,7 +44,8 @@ class AppLockScreenViewModelTests: XCTestCase {
|
||||
|
||||
// When entering it on the lock screen.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .appUnlocked }
|
||||
context.send(viewAction: .submitPINCode(pinCode))
|
||||
viewModel.context.pinCode = pinCode
|
||||
context.send(viewAction: .submitPINCode)
|
||||
let result = try await deferred.fulfill()
|
||||
|
||||
// The app should become unlocked.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6d8b859217f475cd3477e96e8f501662d7c288d731800112d97af0d8c7718273
|
||||
size 129515
|
||||
1
changelog.d/pr-1925.wip
Normal file
1
changelog.d/pr-1925.wip
Normal file
@@ -0,0 +1 @@
|
||||
Implement the AppLockScreen as per the designs.
|
||||
Reference in New Issue
Block a user