Initial implementation for AppLockSetupFlowCoordinator. (#1949)

Rename AppLockSettingsScreen to AppLockSetupSettingsScreen.
Implement forced logout and persist PIN entry attempts.
This commit is contained in:
Doug
2023-10-25 12:03:37 +01:00
committed by GitHub
parent 30b18031a0
commit c6e4dc2a37
53 changed files with 773 additions and 234 deletions
+40 -40
View File
@@ -10,7 +10,6 @@
000765812BABB81F5174C601 /* AppLockSetupPINScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEDC38E389B89BCF5C1AFD4A /* AppLockSetupPINScreenUITests.swift */; }; 000765812BABB81F5174C601 /* AppLockSetupPINScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEDC38E389B89BCF5C1AFD4A /* AppLockSetupPINScreenUITests.swift */; };
0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070FD43DC6BF4E50217965A /* LocalizationTests.swift */; }; 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070FD43DC6BF4E50217965A /* LocalizationTests.swift */; };
0180C44B997EDA8D21F883AC /* RoomNotificationSettingsCustomSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B746EFA112532A7B701FB914 /* RoomNotificationSettingsCustomSectionView.swift */; }; 0180C44B997EDA8D21F883AC /* RoomNotificationSettingsCustomSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B746EFA112532A7B701FB914 /* RoomNotificationSettingsCustomSectionView.swift */; };
0206016CCEF6EF9365916768 /* AppLockSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33284693F54382F46CFD2EDD /* AppLockSettingsScreenViewModelProtocol.swift */; };
020C530986D7B97631877FEF /* TimelineItemMacContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */; }; 020C530986D7B97631877FEF /* TimelineItemMacContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */; };
020F7E70167FB2833266F2F0 /* AnalyticsSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39D7F513A36C9C1951DB44C /* AnalyticsSettingsScreen.swift */; }; 020F7E70167FB2833266F2F0 /* AnalyticsSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39D7F513A36C9C1951DB44C /* AnalyticsSettingsScreen.swift */; };
024E70451A7CD9E4E034D8A9 /* VoiceMessageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B976F8B2AA654D923422 /* VoiceMessageRoomTimelineItem.swift */; }; 024E70451A7CD9E4E034D8A9 /* VoiceMessageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B976F8B2AA654D923422 /* VoiceMessageRoomTimelineItem.swift */; };
@@ -45,7 +44,6 @@
0A194F5E70B5A628C1BF4476 /* AdvancedSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */; }; 0A194F5E70B5A628C1BF4476 /* AdvancedSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */; };
0AA0477E063E72B786A983CF /* AnalyticsPromptScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E1FF2DA52B1DE7CAEC5422 /* AnalyticsPromptScreenViewModel.swift */; }; 0AA0477E063E72B786A983CF /* AnalyticsPromptScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E1FF2DA52B1DE7CAEC5422 /* AnalyticsPromptScreenViewModel.swift */; };
0ACAA31FD0399CEEBA3ECC21 /* UserDetailsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */; }; 0ACAA31FD0399CEEBA3ECC21 /* UserDetailsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */; };
0AD81E04A8C024C09B7AEAC5 /* AppLockSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86F43BF1C26CB31B4BFA610 /* AppLockSettingsScreenModels.swift */; };
0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; }; 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; };
0B57C2399B9E1CE5CE0D8005 /* ComposerToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D121B4FCFC38DBCC17BCC6D6 /* ComposerToolbar.swift */; }; 0B57C2399B9E1CE5CE0D8005 /* ComposerToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D121B4FCFC38DBCC17BCC6D6 /* ComposerToolbar.swift */; };
0BDA19079FD6E17C5AC62E22 /* RoomDetailsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */; }; 0BDA19079FD6E17C5AC62E22 /* RoomDetailsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */; };
@@ -197,7 +195,6 @@
355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */; }; 355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */; };
3582056513A384F110EC8274 /* MediaPlayerProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */; }; 3582056513A384F110EC8274 /* MediaPlayerProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */; };
35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; }; 35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; };
3627DFEE96824E0E2EA69B88 /* AppLockSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6615CBDE154455007F456DBB /* AppLockSettingsScreen.swift */; };
366D5BFE52CB79E804C7D095 /* CallScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */; }; 366D5BFE52CB79E804C7D095 /* CallScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */; };
368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; }; 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; };
36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; }; 36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; };
@@ -265,6 +262,7 @@
4714991754A08B58B4D7ED85 /* OnboardingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F27BAB69EB568369F1F6B3 /* OnboardingScreenViewModelProtocol.swift */; }; 4714991754A08B58B4D7ED85 /* OnboardingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F27BAB69EB568369F1F6B3 /* OnboardingScreenViewModelProtocol.swift */; };
47305C0911C9E1AA774A4000 /* TemplateScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */; }; 47305C0911C9E1AA774A4000 /* TemplateScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */; };
4799A852132F1744E2825994 /* CreateRoomViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */; }; 4799A852132F1744E2825994 /* CreateRoomViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */; };
4807E8F51DB54F56B25E1C7E /* AppLockSetupSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C38663020DF2EB2D13F5E /* AppLockSetupSettingsScreenViewModel.swift */; };
484202C5D50983442D24D061 /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; }; 484202C5D50983442D24D061 /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; };
491D62ACD19E6F134B1766AF /* RoomNotificationSettingsUserDefinedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3203C6566DC17B7AECC1B7FD /* RoomNotificationSettingsUserDefinedScreen.swift */; }; 491D62ACD19E6F134B1766AF /* RoomNotificationSettingsUserDefinedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3203C6566DC17B7AECC1B7FD /* RoomNotificationSettingsUserDefinedScreen.swift */; };
492274DA6691EE985C2FCCAA /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 67E7A6F388D3BF85767609D9 /* Sentry */; }; 492274DA6691EE985C2FCCAA /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 67E7A6F388D3BF85767609D9 /* Sentry */; };
@@ -282,6 +280,7 @@
4C356F5CCB4CDC99BFA45185 /* AppLockSetupPINScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7884BD256C091EB511B2EDF /* AppLockSetupPINScreenViewModelProtocol.swift */; }; 4C356F5CCB4CDC99BFA45185 /* AppLockSetupPINScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7884BD256C091EB511B2EDF /* AppLockSetupPINScreenViewModelProtocol.swift */; };
4C5A638DAA8AF64565BA4866 /* EncryptedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5351EBD7A0B9610548E4B7B2 /* EncryptedRoomTimelineItem.swift */; }; 4C5A638DAA8AF64565BA4866 /* EncryptedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5351EBD7A0B9610548E4B7B2 /* EncryptedRoomTimelineItem.swift */; };
4E0D9E09B52CEC4C0E6211A8 /* MediaPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */; }; 4E0D9E09B52CEC4C0E6211A8 /* MediaPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */; };
4E8A2A2CFEB212F14E49E1A1 /* AppLockSetupSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */; };
4E8F17EBA24FBBA6ABB62ECB /* MockBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3948D16F021DFDB2CD26EAA8 /* MockBackgroundTaskService.swift */; }; 4E8F17EBA24FBBA6ABB62ECB /* MockBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3948D16F021DFDB2CD26EAA8 /* MockBackgroundTaskService.swift */; };
4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; }; 4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; };
4EA1CE0E88EA68E862FF0EA2 /* NotificationSettingsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */; }; 4EA1CE0E88EA68E862FF0EA2 /* NotificationSettingsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */; };
@@ -352,7 +351,6 @@
6189B4ABD535CE526FA1107B /* StartChatViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */; }; 6189B4ABD535CE526FA1107B /* StartChatViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */; };
61941DEE5F3834765770BE01 /* InviteUsersScreenSelectedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F32E0B4B83D2A11EE8D011 /* InviteUsersScreenSelectedItem.swift */; }; 61941DEE5F3834765770BE01 /* InviteUsersScreenSelectedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F32E0B4B83D2A11EE8D011 /* InviteUsersScreenSelectedItem.swift */; };
61A36B9BB2ADE36CEFF5E98C /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E93A1BE7D8A2EBCAD51EEB4 /* Array.swift */; }; 61A36B9BB2ADE36CEFF5E98C /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E93A1BE7D8A2EBCAD51EEB4 /* Array.swift */; };
61C345258DD392477E79A3B5 /* AppLockSettingsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F088B61525099A48909743B /* AppLockSettingsScreenUITests.swift */; };
6213C897001F953E21D3CC16 /* CompletionSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCF54536699ACEE3DB6BA3CB /* CompletionSuggestionService.swift */; }; 6213C897001F953E21D3CC16 /* CompletionSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCF54536699ACEE3DB6BA3CB /* CompletionSuggestionService.swift */; };
62418EA4E3EB597AD184AEB6 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; }; 62418EA4E3EB597AD184AEB6 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; };
62910B515BCB4B455E24D7C1 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */; }; 62910B515BCB4B455E24D7C1 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */; };
@@ -400,6 +398,7 @@
6CD61FAF03E8986523C2ABB8 /* StartChatScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3005886F00029F058DB62BE /* StartChatScreenCoordinator.swift */; }; 6CD61FAF03E8986523C2ABB8 /* StartChatScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3005886F00029F058DB62BE /* StartChatScreenCoordinator.swift */; };
6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; }; 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; };
6E47D126DD7585E8F8237CE7 /* LoadableAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */; }; 6E47D126DD7585E8F8237CE7 /* LoadableAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */; };
6E4E401BE97AC241DA7C7716 /* AppLockSetupSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502F986D57158674172C58E3 /* AppLockSetupSettingsScreenModels.swift */; };
6E63704717F17593A475D152 /* RoomNotificationSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA14564EE143F73F7E4D1F79 /* RoomNotificationSettingsScreenModels.swift */; }; 6E63704717F17593A475D152 /* RoomNotificationSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA14564EE143F73F7E4D1F79 /* RoomNotificationSettingsScreenModels.swift */; };
6EC7A40A537CFB3D526A111C /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; 6EC7A40A537CFB3D526A111C /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; };
6F26CBC84AE87EB4068D398B /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 78B28D75FF7AF8E6146DEE2A /* LRUCache */; }; 6F26CBC84AE87EB4068D398B /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 78B28D75FF7AF8E6146DEE2A /* LRUCache */; };
@@ -480,7 +479,6 @@
829062DD3C3F7016FE1A6476 /* RoomDetailsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */; }; 829062DD3C3F7016FE1A6476 /* RoomDetailsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */; };
8317E1314C00DCCC99D30DA8 /* TextBasedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9227F7495DA43324050A863 /* TextBasedRoomTimelineItem.swift */; }; 8317E1314C00DCCC99D30DA8 /* TextBasedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9227F7495DA43324050A863 /* TextBasedRoomTimelineItem.swift */; };
83A4DAB181C56987C3E804FF /* MapTilerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */; }; 83A4DAB181C56987C3E804FF /* MapTilerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */; };
84215E902C62E9B8E8AB79F0 /* AppLockSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267C0279BB8D907E2C40DDCA /* AppLockSettingsScreenCoordinator.swift */; };
84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; }; 84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; };
8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */; }; 8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */; };
847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1D4A6D451F43A03CACD01D /* PINTextField.swift */; }; 847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1D4A6D451F43A03CACD01D /* PINTextField.swift */; };
@@ -532,6 +530,7 @@
8D3E1FADD78E72504DE0E402 /* UserAgentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */; }; 8D3E1FADD78E72504DE0E402 /* UserAgentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */; };
8D605456793F243649EC96AA /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = CD6B0C4639E066915B5E6463 /* target.yml */; }; 8D605456793F243649EC96AA /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = CD6B0C4639E066915B5E6463 /* target.yml */; };
8D71E5E53F372202379BECCE /* BugReportScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303FCADE77DF1F3670C086ED /* BugReportScreenViewModel.swift */; }; 8D71E5E53F372202379BECCE /* BugReportScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303FCADE77DF1F3670C086ED /* BugReportScreenViewModel.swift */; };
8DCD9CC5361FF22A5B2C20F1 /* AppLockSetupSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9FCE4D1E3A81AC1CC5CB91 /* AppLockSetupSettingsScreenCoordinator.swift */; };
8DDC6F28C797D8685F2F8E32 /* AnalyticsConsentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57B6B383F1FD04CC0E7B60C6 /* AnalyticsConsentState.swift */; }; 8DDC6F28C797D8685F2F8E32 /* AnalyticsConsentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57B6B383F1FD04CC0E7B60C6 /* AnalyticsConsentState.swift */; };
8E650379587C31D7912ED67B /* UNNotification+Creator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */; }; 8E650379587C31D7912ED67B /* UNNotification+Creator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */; };
8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */; }; 8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */; };
@@ -635,7 +634,6 @@
A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */; }; A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */; };
A743841F91B62B0E56217B04 /* SecureBackupKeyBackupScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DCB219D7B7B0299358FF81 /* SecureBackupKeyBackupScreenUITests.swift */; }; A743841F91B62B0E56217B04 /* SecureBackupKeyBackupScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DCB219D7B7B0299358FF81 /* SecureBackupKeyBackupScreenUITests.swift */; };
A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; }; A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; };
A7BEE8216B4B12BE4C0F2C3F /* AppLockSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892EF45CCC5D2BF0FD1F770C /* AppLockSettingsScreenViewModel.swift */; };
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; };
A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; }; A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; };
A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; }; A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; };
@@ -673,6 +671,7 @@
B1069F361E604D5436AE9FFD /* StaticLocationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B06663F7858E45882E63471 /* StaticLocationScreen.swift */; }; B1069F361E604D5436AE9FFD /* StaticLocationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B06663F7858E45882E63471 /* StaticLocationScreen.swift */; };
B1387648C6F71F1B98244803 /* SecureBackupRecoveryKeyScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 596AA8843AC1A234F3387767 /* SecureBackupRecoveryKeyScreenCoordinator.swift */; }; B1387648C6F71F1B98244803 /* SecureBackupRecoveryKeyScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 596AA8843AC1A234F3387767 /* SecureBackupRecoveryKeyScreenCoordinator.swift */; };
B14BC354E56616B6B7D9A3D7 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */; }; B14BC354E56616B6B7D9A3D7 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */; };
B188D0907A4D38AAAF6FEFA8 /* AppLockSetupFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DBB08A95EFA668F2CF27211 /* AppLockSetupFlowCoordinator.swift */; };
B22D857D1E8FCA6DD74A58E3 /* UserSessionScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F899D02CF26EA7675EEBE74C /* UserSessionScreenTests.swift */; }; B22D857D1E8FCA6DD74A58E3 /* UserSessionScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F899D02CF26EA7675EEBE74C /* UserSessionScreenTests.swift */; };
B245583C63F8F90357B87FAE /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = A2AE110B053B55E38F8D10C7 /* KZFileWatchers */; }; B245583C63F8F90357B87FAE /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = A2AE110B053B55E38F8D10C7 /* KZFileWatchers */; };
B27D3190784F85916DA1C394 /* SessionVerificationScreenStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B1EE0908B2BF9212436AD3E /* SessionVerificationScreenStateMachine.swift */; }; B27D3190784F85916DA1C394 /* SessionVerificationScreenStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B1EE0908B2BF9212436AD3E /* SessionVerificationScreenStateMachine.swift */; };
@@ -766,6 +765,7 @@
C7774720A4B2E34693E3227C /* RoomNotificationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8896CDD20CA2D87EA3B848A1 /* RoomNotificationSettingsScreen.swift */; }; C7774720A4B2E34693E3227C /* RoomNotificationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8896CDD20CA2D87EA3B848A1 /* RoomNotificationSettingsScreen.swift */; };
C7ABEBECDC513F7887DACF66 /* ProgressMaskModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68010886142843705E342645 /* ProgressMaskModifier.swift */; }; C7ABEBECDC513F7887DACF66 /* ProgressMaskModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68010886142843705E342645 /* ProgressMaskModifier.swift */; };
C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */; }; C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */; };
C85C7A201E4CFDA477ACEBEB /* AppLockSetupSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8610C1D21565C950BCA6A454 /* AppLockSetupSettingsScreenViewModelProtocol.swift */; };
C8BD80891BAD688EF2C15CDB /* MediaUploadPreviewScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DD0855F2F76D47E5555082 /* MediaUploadPreviewScreenCoordinator.swift */; }; C8BD80891BAD688EF2C15CDB /* MediaUploadPreviewScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DD0855F2F76D47E5555082 /* MediaUploadPreviewScreenCoordinator.swift */; };
C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */; }; C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */; };
C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E0B4A34E69BD2132BEC521 /* MessageText.swift */; }; C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E0B4A34E69BD2132BEC521 /* MessageText.swift */; };
@@ -1066,6 +1066,7 @@
0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
0D0B159AFFBBD8ECFD0E37FA /* LoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = "<group>"; }; 0D0B159AFFBBD8ECFD0E37FA /* LoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = "<group>"; };
0D8F620C8B314840D8602E3F /* NSE.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = NSE.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 0D8F620C8B314840D8602E3F /* NSE.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = NSE.appex; sourceTree = BUILT_PRODUCTS_DIR; };
0DBB08A95EFA668F2CF27211 /* AppLockSetupFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupFlowCoordinator.swift; sourceTree = "<group>"; };
0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentionalMentions.swift; sourceTree = "<group>"; }; 0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentionalMentions.swift; sourceTree = "<group>"; };
0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStore.swift; sourceTree = "<group>"; }; 0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStore.swift; sourceTree = "<group>"; };
0EA689E792E679F5E3956F21 /* UITimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITimelineView.swift; sourceTree = "<group>"; }; 0EA689E792E679F5E3956F21 /* UITimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITimelineView.swift; sourceTree = "<group>"; };
@@ -1131,6 +1132,7 @@
1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxyProtocol.swift; sourceTree = "<group>"; }; 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxyProtocol.swift; sourceTree = "<group>"; };
1D67E616BCA82D8A1258D488 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; }; 1D67E616BCA82D8A1258D488 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; };
1D8866FE1CCCF10305FCACBC /* CallScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenUITests.swift; sourceTree = "<group>"; }; 1D8866FE1CCCF10305FCACBC /* CallScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenUITests.swift; sourceTree = "<group>"; };
1D8C38663020DF2EB2D13F5E /* AppLockSetupSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenViewModel.swift; sourceTree = "<group>"; };
1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = "<group>"; }; 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = "<group>"; };
1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelProtocol.swift; sourceTree = "<group>"; };
1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProposedViewSize.swift; sourceTree = "<group>"; }; 1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProposedViewSize.swift; sourceTree = "<group>"; };
@@ -1159,7 +1161,6 @@
24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = "<group>"; };
25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = "<group>"; }; 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = "<group>"; };
260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = "<group>"; }; 260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = "<group>"; };
267C0279BB8D907E2C40DDCA /* AppLockSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSettingsScreenCoordinator.swift; sourceTree = "<group>"; };
26B0A96B8FE4849227945067 /* VoiceMessageRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorder.swift; sourceTree = "<group>"; }; 26B0A96B8FE4849227945067 /* VoiceMessageRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorder.swift; sourceTree = "<group>"; };
26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceProtocol.swift; sourceTree = "<group>"; }; 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceProtocol.swift; sourceTree = "<group>"; };
2757B1BE23DF8AA239937243 /* AudioConverterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverterProtocol.swift; sourceTree = "<group>"; }; 2757B1BE23DF8AA239937243 /* AudioConverterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverterProtocol.swift; sourceTree = "<group>"; };
@@ -1200,7 +1201,6 @@
3203C6566DC17B7AECC1B7FD /* RoomNotificationSettingsUserDefinedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsUserDefinedScreen.swift; sourceTree = "<group>"; }; 3203C6566DC17B7AECC1B7FD /* RoomNotificationSettingsUserDefinedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsUserDefinedScreen.swift; sourceTree = "<group>"; };
32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyProtocol.swift; sourceTree = "<group>"; }; 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyProtocol.swift; sourceTree = "<group>"; };
32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelTests.swift; sourceTree = "<group>"; }; 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelTests.swift; sourceTree = "<group>"; };
33284693F54382F46CFD2EDD /* AppLockSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
33649299575BADC34924ABC6 /* InvitesScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenCoordinator.swift; sourceTree = "<group>"; }; 33649299575BADC34924ABC6 /* InvitesScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenCoordinator.swift; sourceTree = "<group>"; };
33720F7AD25E85E4A84669E8 /* MapTilerGeocoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerGeocoding.swift; sourceTree = "<group>"; }; 33720F7AD25E85E4A84669E8 /* MapTilerGeocoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerGeocoding.swift; sourceTree = "<group>"; };
33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = "<group>"; }; 33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -1237,6 +1237,7 @@
3D1D4A6D451F43A03CACD01D /* PINTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextField.swift; sourceTree = "<group>"; }; 3D1D4A6D451F43A03CACD01D /* PINTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextField.swift; sourceTree = "<group>"; };
3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = "<group>"; }; 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = "<group>"; };
3D4DD336905C72F95EAF34B7 /* ElementX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ElementX-Bridging-Header.h"; sourceTree = "<group>"; }; 3D4DD336905C72F95EAF34B7 /* ElementX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ElementX-Bridging-Header.h"; sourceTree = "<group>"; };
3D9FCE4D1E3A81AC1CC5CB91 /* AppLockSetupSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenCoordinator.swift; sourceTree = "<group>"; };
3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = "<group>"; }; 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = "<group>"; };
3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTaskService.swift; sourceTree = "<group>"; }; 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTaskService.swift; sourceTree = "<group>"; };
3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItem.swift; sourceTree = "<group>"; }; 3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItem.swift; sourceTree = "<group>"; };
@@ -1305,6 +1306,7 @@
4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinatorTests.swift; sourceTree = "<group>"; }; 4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinatorTests.swift; sourceTree = "<group>"; };
4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = "<group>"; }; 4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = "<group>"; };
4FDD775CFD72DD2D3C8A8390 /* NotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyProtocol.swift; sourceTree = "<group>"; }; 4FDD775CFD72DD2D3C8A8390 /* NotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyProtocol.swift; sourceTree = "<group>"; };
502F986D57158674172C58E3 /* AppLockSetupSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenModels.swift; sourceTree = "<group>"; };
505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelTests.swift; sourceTree = "<group>"; }; 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelTests.swift; sourceTree = "<group>"; };
5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItem.swift; sourceTree = "<group>"; }; 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItem.swift; sourceTree = "<group>"; };
50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixUserShareLink.swift; sourceTree = "<group>"; }; 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixUserShareLink.swift; sourceTree = "<group>"; };
@@ -1323,6 +1325,7 @@
536E72DCBEEC4A1FE66CFDCE /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; }; 536E72DCBEEC4A1FE66CFDCE /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
542D4F49FABA056DEEEB3400 /* RustTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RustTracing.swift; sourceTree = "<group>"; }; 542D4F49FABA056DEEEB3400 /* RustTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RustTracing.swift; sourceTree = "<group>"; };
5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; }; 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreen.swift; sourceTree = "<group>"; };
54C4E7B46099462F12000C91 /* DeveloperOptionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 54C4E7B46099462F12000C91 /* DeveloperOptionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemSender.swift; sourceTree = "<group>"; }; 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemSender.swift; sourceTree = "<group>"; };
55F30E764BED111C81739844 /* SoftLogoutUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutUITests.swift; sourceTree = "<group>"; }; 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutUITests.swift; sourceTree = "<group>"; };
@@ -1355,7 +1358,6 @@
5D99730313BEBF08CDE81EE3 /* EmojiDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetection.swift; sourceTree = "<group>"; }; 5D99730313BEBF08CDE81EE3 /* EmojiDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetection.swift; sourceTree = "<group>"; };
5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProperties.swift; sourceTree = "<group>"; }; 5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProperties.swift; sourceTree = "<group>"; };
5EB2CAA266B921D128C35710 /* LegalInformationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenCoordinator.swift; sourceTree = "<group>"; }; 5EB2CAA266B921D128C35710 /* LegalInformationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenCoordinator.swift; sourceTree = "<group>"; };
5F088B61525099A48909743B /* AppLockSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSettingsScreenUITests.swift; sourceTree = "<group>"; };
5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = "<group>"; }; 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = "<group>"; };
5F8002D0392A476D2758B291 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = "<group>"; }; 5F8002D0392A476D2758B291 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = "<group>"; };
6033779EB37259F27F938937 /* ClientProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyProtocol.swift; sourceTree = "<group>"; }; 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyProtocol.swift; sourceTree = "<group>"; };
@@ -1375,7 +1377,6 @@
6569593FA36B22259E806A67 /* AudioRecorderState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderState.swift; sourceTree = "<group>"; }; 6569593FA36B22259E806A67 /* AudioRecorderState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderState.swift; sourceTree = "<group>"; };
65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryService.swift; sourceTree = "<group>"; }; 65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryService.swift; sourceTree = "<group>"; };
65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationServiceProxy.swift; sourceTree = "<group>"; }; 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationServiceProxy.swift; sourceTree = "<group>"; };
6615CBDE154455007F456DBB /* AppLockSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSettingsScreen.swift; sourceTree = "<group>"; };
6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
667DD3A9D932D7D9EB380CAA /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sk; path = sk.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 667DD3A9D932D7D9EB380CAA /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sk; path = sk.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
66901977F6469D03C333DF32 /* RoomNotificationSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenUITests.swift; sourceTree = "<group>"; }; 66901977F6469D03C333DF32 /* RoomNotificationSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenUITests.swift; sourceTree = "<group>"; };
@@ -1481,6 +1482,7 @@
8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelTests.swift; sourceTree = "<group>"; }; 8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelTests.swift; sourceTree = "<group>"; };
854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = "<group>"; }; 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = "<group>"; };
85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = "<group>"; }; 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = "<group>"; };
8610C1D21565C950BCA6A454 /* AppLockSetupSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
86376BEE425704AEE197CA54 /* PillContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillContext.swift; sourceTree = "<group>"; }; 86376BEE425704AEE197CA54 /* PillContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillContext.swift; sourceTree = "<group>"; };
86873A768B13069BB5CAECF6 /* InvitesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 86873A768B13069BB5CAECF6 /* InvitesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenViewModelProtocol.swift; sourceTree = "<group>"; };
86A6F283BC574FDB96ABBB07 /* DeveloperOptionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModel.swift; sourceTree = "<group>"; }; 86A6F283BC574FDB96ABBB07 /* DeveloperOptionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModel.swift; sourceTree = "<group>"; };
@@ -1491,7 +1493,6 @@
889DEDD63C68ABDA8AD29812 /* VoiceMessageMediaManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManagerProtocol.swift; sourceTree = "<group>"; }; 889DEDD63C68ABDA8AD29812 /* VoiceMessageMediaManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManagerProtocol.swift; sourceTree = "<group>"; };
89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerStateTests.swift; sourceTree = "<group>"; }; 89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerStateTests.swift; sourceTree = "<group>"; };
892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = "<group>"; }; 892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = "<group>"; };
892EF45CCC5D2BF0FD1F770C /* AppLockSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSettingsScreenViewModel.swift; sourceTree = "<group>"; };
893777A4997BBDB68079D4F5 /* ArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayTests.swift; sourceTree = "<group>"; }; 893777A4997BBDB68079D4F5 /* ArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayTests.swift; sourceTree = "<group>"; };
8977176AB534AA41630395BC /* LegalInformationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 8977176AB534AA41630395BC /* LegalInformationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
897DF5E9A70CE05A632FC8AF /* UTType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTType.swift; sourceTree = "<group>"; }; 897DF5E9A70CE05A632FC8AF /* UTType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTType.swift; sourceTree = "<group>"; };
@@ -1655,7 +1656,6 @@
B81B6170DB690013CEB646F4 /* MapLibreModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreModels.swift; sourceTree = "<group>"; }; B81B6170DB690013CEB646F4 /* MapLibreModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreModels.swift; sourceTree = "<group>"; };
B83BC0DC9A2DF2DD60F9B6E9 /* NotificationSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenUITests.swift; sourceTree = "<group>"; }; B83BC0DC9A2DF2DD60F9B6E9 /* NotificationSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenUITests.swift; sourceTree = "<group>"; };
B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = "<group>"; }; B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = "<group>"; };
B86F43BF1C26CB31B4BFA610 /* AppLockSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSettingsScreenModels.swift; sourceTree = "<group>"; };
B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = "<group>"; }; B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = "<group>"; };
B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = "<group>"; }; B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = "<group>"; };
B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = "<group>"; }; B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = "<group>"; };
@@ -2022,14 +2022,6 @@
path = VoiceMessage; path = VoiceMessage;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
01FACE97F4C9E9F522D401EB /* View */ = {
isa = PBXGroup;
children = (
6615CBDE154455007F456DBB /* AppLockSettingsScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
0210F4932B59277E2EEEF7BC /* RoomNotificationSettingsScreen */ = { 0210F4932B59277E2EEEF7BC /* RoomNotificationSettingsScreen */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -2211,9 +2203,9 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3AD37D7DDF9904587601239D /* AppLockScreen */, 3AD37D7DDF9904587601239D /* AppLockScreen */,
CE39C9B97963CC30AB0859E5 /* AppLockSettingsScreen */,
4570BFC8DD6665A91381F400 /* AppLockSetupBiometricsScreen */, 4570BFC8DD6665A91381F400 /* AppLockSetupBiometricsScreen */,
570026F1BA71A2D167652E48 /* AppLockSetupPINScreen */, 570026F1BA71A2D167652E48 /* AppLockSetupPINScreen */,
2AC6FD695C6F79F67C056463 /* AppLockSetupSettingsScreen */,
FB039572AA54E0690B4051AD /* Common */, FB039572AA54E0690B4051AD /* Common */,
); );
path = AppLock; path = AppLock;
@@ -2347,6 +2339,18 @@
path = ComposerToolbar; path = ComposerToolbar;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
2AC6FD695C6F79F67C056463 /* AppLockSetupSettingsScreen */ = {
isa = PBXGroup;
children = (
3D9FCE4D1E3A81AC1CC5CB91 /* AppLockSetupSettingsScreenCoordinator.swift */,
502F986D57158674172C58E3 /* AppLockSetupSettingsScreenModels.swift */,
1D8C38663020DF2EB2D13F5E /* AppLockSetupSettingsScreenViewModel.swift */,
8610C1D21565C950BCA6A454 /* AppLockSetupSettingsScreenViewModelProtocol.swift */,
5C3CC0EA3D5B1E74EC71A463 /* View */,
);
path = AppLockSetupSettingsScreen;
sourceTree = "<group>";
};
2C0F49BD446849654C0D24E0 /* RoomMember */ = { 2C0F49BD446849654C0D24E0 /* RoomMember */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -2928,6 +2932,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */, FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */,
0DBB08A95EFA668F2CF27211 /* AppLockSetupFlowCoordinator.swift */,
9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */, 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */,
C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */, C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */,
E8774CF614849664B5B3C2A1 /* UserSessionFlowCoordinatorStateMachine.swift */, E8774CF614849664B5B3C2A1 /* UserSessionFlowCoordinatorStateMachine.swift */,
@@ -2961,6 +2966,14 @@
path = SoftLogoutScreen; path = SoftLogoutScreen;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
5C3CC0EA3D5B1E74EC71A463 /* View */ = {
isa = PBXGroup;
children = (
5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
5DC032EC5F19009AA7CADB41 /* View */ = { 5DC032EC5F19009AA7CADB41 /* View */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -3662,7 +3675,6 @@
16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.swift */, 16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.swift */,
7D0CBC76C80E04345E11F2DB /* Application.swift */, 7D0CBC76C80E04345E11F2DB /* Application.swift */,
349C633291427A0F29C28C54 /* AppLockScreenUITests.swift */, 349C633291427A0F29C28C54 /* AppLockScreenUITests.swift */,
5F088B61525099A48909743B /* AppLockSettingsScreenUITests.swift */,
AEDC38E389B89BCF5C1AFD4A /* AppLockSetupPINScreenUITests.swift */, AEDC38E389B89BCF5C1AFD4A /* AppLockSetupPINScreenUITests.swift */,
5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */, 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */,
C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */,
@@ -4280,18 +4292,6 @@
path = Layout; path = Layout;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
CE39C9B97963CC30AB0859E5 /* AppLockSettingsScreen */ = {
isa = PBXGroup;
children = (
267C0279BB8D907E2C40DDCA /* AppLockSettingsScreenCoordinator.swift */,
B86F43BF1C26CB31B4BFA610 /* AppLockSettingsScreenModels.swift */,
892EF45CCC5D2BF0FD1F770C /* AppLockSettingsScreenViewModel.swift */,
33284693F54382F46CFD2EDD /* AppLockSettingsScreenViewModelProtocol.swift */,
01FACE97F4C9E9F522D401EB /* View */,
);
path = AppLockSettingsScreen;
sourceTree = "<group>";
};
D4DB8163C10389C069458252 /* RoomMemberListScreen */ = { D4DB8163C10389C069458252 /* RoomMemberListScreen */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -5277,21 +5277,22 @@
E79D79CDAFE8BEBCC3AECA54 /* AppLockScreenViewModelProtocol.swift in Sources */, E79D79CDAFE8BEBCC3AECA54 /* AppLockScreenViewModelProtocol.swift in Sources */,
1D623953F970D11F6F38499C /* AppLockService.swift in Sources */, 1D623953F970D11F6F38499C /* AppLockService.swift in Sources */,
D2048FD56760BDABA3DB5FC2 /* AppLockServiceProtocol.swift in Sources */, D2048FD56760BDABA3DB5FC2 /* AppLockServiceProtocol.swift in Sources */,
3627DFEE96824E0E2EA69B88 /* AppLockSettingsScreen.swift in Sources */,
84215E902C62E9B8E8AB79F0 /* AppLockSettingsScreenCoordinator.swift in Sources */,
0AD81E04A8C024C09B7AEAC5 /* AppLockSettingsScreenModels.swift in Sources */,
A7BEE8216B4B12BE4C0F2C3F /* AppLockSettingsScreenViewModel.swift in Sources */,
0206016CCEF6EF9365916768 /* AppLockSettingsScreenViewModelProtocol.swift in Sources */,
BB9B800C6094E34860E89DC5 /* AppLockSetupBiometricsScreen.swift in Sources */, BB9B800C6094E34860E89DC5 /* AppLockSetupBiometricsScreen.swift in Sources */,
8015842CB4DE1BE414D2CDED /* AppLockSetupBiometricsScreenCoordinator.swift in Sources */, 8015842CB4DE1BE414D2CDED /* AppLockSetupBiometricsScreenCoordinator.swift in Sources */,
21813AF91CFC6F3E3896DB53 /* AppLockSetupBiometricsScreenModels.swift in Sources */, 21813AF91CFC6F3E3896DB53 /* AppLockSetupBiometricsScreenModels.swift in Sources */,
37906355E207DB5703754675 /* AppLockSetupBiometricsScreenViewModel.swift in Sources */, 37906355E207DB5703754675 /* AppLockSetupBiometricsScreenViewModel.swift in Sources */,
9FB41B0E8B2AA9B404E52C8B /* AppLockSetupBiometricsScreenViewModelProtocol.swift in Sources */, 9FB41B0E8B2AA9B404E52C8B /* AppLockSetupBiometricsScreenViewModelProtocol.swift in Sources */,
B188D0907A4D38AAAF6FEFA8 /* AppLockSetupFlowCoordinator.swift in Sources */,
733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */, 733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */,
9696ECAFB4F0C079C5C2A526 /* AppLockSetupPINScreenCoordinator.swift in Sources */, 9696ECAFB4F0C079C5C2A526 /* AppLockSetupPINScreenCoordinator.swift in Sources */,
E4B07FF075C99D04D9AF792D /* AppLockSetupPINScreenModels.swift in Sources */, E4B07FF075C99D04D9AF792D /* AppLockSetupPINScreenModels.swift in Sources */,
0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */, 0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */,
4C356F5CCB4CDC99BFA45185 /* AppLockSetupPINScreenViewModelProtocol.swift in Sources */, 4C356F5CCB4CDC99BFA45185 /* AppLockSetupPINScreenViewModelProtocol.swift in Sources */,
4E8A2A2CFEB212F14E49E1A1 /* AppLockSetupSettingsScreen.swift in Sources */,
8DCD9CC5361FF22A5B2C20F1 /* AppLockSetupSettingsScreenCoordinator.swift in Sources */,
6E4E401BE97AC241DA7C7716 /* AppLockSetupSettingsScreenModels.swift in Sources */,
4807E8F51DB54F56B25E1C7E /* AppLockSetupSettingsScreenViewModel.swift in Sources */,
C85C7A201E4CFDA477ACEBEB /* AppLockSetupSettingsScreenViewModelProtocol.swift in Sources */,
EF890DEF0479E66548F2BA23 /* AppLockTimer.swift in Sources */, EF890DEF0479E66548F2BA23 /* AppLockTimer.swift in Sources */,
355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */, 355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */,
12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */, 12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */,
@@ -5908,7 +5909,6 @@
8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */, 8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */,
BF675964C9159F718589C36A /* AnalyticsSettingsScreenUITests.swift in Sources */, BF675964C9159F718589C36A /* AnalyticsSettingsScreenUITests.swift in Sources */,
F05516474DB42369FD976CEF /* AppLockScreenUITests.swift in Sources */, F05516474DB42369FD976CEF /* AppLockScreenUITests.swift in Sources */,
61C345258DD392477E79A3B5 /* AppLockSettingsScreenUITests.swift in Sources */,
000765812BABB81F5174C601 /* AppLockSetupPINScreenUITests.swift in Sources */, 000765812BABB81F5174C601 /* AppLockSetupPINScreenUITests.swift in Sources */,
7405B4824D45BA7C3D943E76 /* Application.swift in Sources */, 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */,
ACF094CF3BF02DBFA6DFDE60 /* AuthenticationCoordinatorUITests.swift in Sources */, ACF094CF3BF02DBFA6DFDE60 /* AuthenticationCoordinatorUITests.swift in Sources */,
@@ -144,6 +144,7 @@
"common_server_url" = "Server URL"; "common_server_url" = "Server URL";
"common_settings" = "Settings"; "common_settings" = "Settings";
"common_shared_location" = "Shared location"; "common_shared_location" = "Shared location";
"common_signing_out" = "Signing out";
"common_starting_chat" = "Starting chat…"; "common_starting_chat" = "Starting chat…";
"common_sticker" = "Sticker"; "common_sticker" = "Sticker";
"common_success" = "Success"; "common_success" = "Success";
@@ -297,7 +298,6 @@
"screen_app_lock_setup_pin_mismatch_dialog_title" = "PINs don't match"; "screen_app_lock_setup_pin_mismatch_dialog_title" = "PINs don't match";
"screen_app_lock_signout_alert_message" = "You’ll need to re-login and create a new PIN to proceed"; "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_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_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" = "You may contact me if you have any follow up questions.";
"screen_bug_report_contact_me_title" = "Contact me"; "screen_bug_report_contact_me_title" = "Contact me";
@@ -162,6 +162,22 @@
<string>%1$d room changes</string> <string>%1$d room changes</string>
</dict> </dict>
</dict> </dict>
<key>screen_app_lock_subtitle</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>You have %1$d attempt to unlock</string>
<key>other</key>
<string>You have %1$d attempts to unlock</string>
</dict>
</dict>
<key>screen_app_lock_subtitle_wrong_pin</key> <key>screen_app_lock_subtitle_wrong_pin</key>
<dict> <dict>
<key>NSStringLocalizedFormatKey</key> <key>NSStringLocalizedFormatKey</key>
@@ -102,8 +102,11 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
backgroundTaskService: backgroundTaskService) backgroundTaskService: backgroundTaskService)
let appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings) let appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings)
let appLockNavigationCoordinator = NavigationRootCoordinator()
let appLockFlowUserIndicatorController = UserIndicatorController(rootCoordinator: appLockNavigationCoordinator)
appLockFlowCoordinator = AppLockFlowCoordinator(appLockService: appLockService, appLockFlowCoordinator = AppLockFlowCoordinator(appLockService: appLockService,
navigationCoordinator: NavigationRootCoordinator()) userIndicatorController: appLockFlowUserIndicatorController,
navigationCoordinator: appLockNavigationCoordinator)
notificationManager = NotificationManager(notificationCenter: UNUserNotificationCenter.current(), notificationManager = NotificationManager(notificationCenter: UNUserNotificationCenter.current(),
appSettings: appSettings) appSettings: appSettings)
@@ -329,12 +332,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
// We can ignore signOut when already in the process of signing out, // We can ignore signOut when already in the process of signing out,
// such as the SDK sending an authError due to token invalidation. // such as the SDK sending an authError due to token invalidation.
break break
case (_, .signOut(let isSoft), .signingOut): case (_, .signOut(let isSoft, _), .signingOut):
self.logout(isSoft: isSoft) self.logout(isSoft: isSoft)
case (.signingOut, .completedSigningOut, .signedOut): case (.signingOut(_, let disableAppLock), .completedSigningOut, .signedOut):
self.presentSplashScreen(isSoftLogout: false) self.presentSplashScreen(isSoftLogout: false, disableAppLock: disableAppLock)
case (.signingOut, .showSoftLogout, .softLogout): case (.signingOut(_, let disableAppLock), .showSoftLogout, .softLogout):
self.presentSplashScreen(isSoftLogout: true) self.presentSplashScreen(isSoftLogout: true, disableAppLock: disableAppLock)
case (.signedIn, .clearCache, .initial): case (.signedIn, .clearCache, .initial):
self.clearCache() self.clearCache()
default: default:
@@ -371,7 +374,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
navigationStackCoordinator: authenticationNavigationStackCoordinator, navigationStackCoordinator: authenticationNavigationStackCoordinator,
appSettings: appSettings, appSettings: appSettings,
analytics: ServiceLocator.shared.analytics, analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController) userIndicatorController: ServiceLocator.shared.userIndicatorController,
appLockService: appLockFlowCoordinator.appLockService)
authenticationCoordinator?.delegate = self authenticationCoordinator?.delegate = self
authenticationCoordinator?.start() authenticationCoordinator?.start()
@@ -410,7 +414,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
stateMachine.processEvent(.createdUserSession) stateMachine.processEvent(.createdUserSession)
case .clearAllData: case .clearAllData:
self.softLogoutCoordinator = nil self.softLogoutCoordinator = nil
stateMachine.processEvent(.signOut(isSoft: false)) stateMachine.processEvent(.signOut(isSoft: false, disableAppLock: false))
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -439,7 +443,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
switch action { switch action {
case .logout: case .logout:
stateMachine.processEvent(.signOut(isSoft: false)) stateMachine.processEvent(.signOut(isSoft: false, disableAppLock: false))
case .clearCache: case .clearCache:
stateMachine.processEvent(.clearCache) stateMachine.processEvent(.clearCache)
} }
@@ -512,7 +516,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
notificationManager.setUserSession(nil) notificationManager.setUserSession(nil)
} }
private func presentSplashScreen(isSoftLogout: Bool = false) { private func presentSplashScreen(isSoftLogout: Bool = false, disableAppLock: Bool = false) {
navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator()) navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator())
if isSoftLogout { if isSoftLogout {
@@ -520,6 +524,15 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
} else { } else {
startAuthentication() startAuthentication()
} }
if disableAppLock {
Task {
// Ensure the navigation stack has settled.
try? await Task.sleep(for: .milliseconds(500))
appLockFlowCoordinator.appLockService.disable()
windowManager.switchToMain()
}
}
} }
private func configureNotificationManager() { private func configureNotificationManager() {
@@ -549,7 +562,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
guard let self else { return } guard let self else { return }
switch callback { switch callback {
case .didReceiveAuthError(let isSoftLogout): case .didReceiveAuthError(let isSoftLogout):
stateMachine.processEvent(.signOut(isSoft: isSoftLogout)) stateMachine.processEvent(.signOut(isSoft: isSoftLogout, disableAppLock: false))
default: default:
break break
} }
@@ -583,6 +596,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
windowManager.switchToAlternate() windowManager.switchToAlternate()
case .unlockApp: case .unlockApp:
windowManager.switchToMain() windowManager.switchToMain()
case .forceLogout:
stateMachine.processEvent(.signOut(isSoft: false, disableAppLock: true))
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -33,7 +33,7 @@ class AppCoordinatorStateMachine {
case signedIn case signedIn
/// Processing a sign out request /// Processing a sign out request
case signingOut(isSoft: Bool) case signingOut(isSoft: Bool, disableAppLock: Bool)
} }
/// Events that can be triggered on the AppCoordinator state machine /// Events that can be triggered on the AppCoordinator state machine
@@ -51,7 +51,7 @@ class AppCoordinatorStateMachine {
case createdUserSession case createdUserSession
/// Request sign out. /// Request sign out.
case signOut(isSoft: Bool) case signOut(isSoft: Bool, disableAppLock: Bool)
/// Request the soft logout screen. /// Request the soft logout screen.
case showSoftLogout case showSoftLogout
/// Signing out completed. /// Signing out completed.
@@ -80,16 +80,17 @@ class AppCoordinatorStateMachine {
stateMachine.addRoutes(event: .createdUserSession, transitions: [.restoringSession => .signedIn]) stateMachine.addRoutes(event: .createdUserSession, transitions: [.restoringSession => .signedIn])
stateMachine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut]) stateMachine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])
stateMachine.addRoutes(event: .completedSigningOut, transitions: [.signingOut(isSoft: false) => .signedOut]) stateMachine.addRoutes(event: .completedSigningOut, transitions: [.signingOut(isSoft: false, disableAppLock: false) => .signedOut,
stateMachine.addRoutes(event: .showSoftLogout, transitions: [.signingOut(isSoft: true) => .softLogout]) .signingOut(isSoft: false, disableAppLock: true) => .signedOut])
stateMachine.addRoutes(event: .showSoftLogout, transitions: [.signingOut(isSoft: true, disableAppLock: false) => .softLogout])
stateMachine.addRoutes(event: .clearCache, transitions: [.signedIn => .initial]) stateMachine.addRoutes(event: .clearCache, transitions: [.signedIn => .initial])
// Transitions with associated values need to be handled through `addRouteMapping` // Transitions with associated values need to be handled through `addRouteMapping`
stateMachine.addRouteMapping { event, fromState, _ in stateMachine.addRouteMapping { event, fromState, _ in
switch (event, fromState) { switch (event, fromState) {
case (.signOut(let isSoft), _): case (.signOut(let isSoft, let disableAppLock), _):
return .signingOut(isSoft: isSoft) return .signingOut(isSoft: isSoft, disableAppLock: disableAppLock)
default: default:
return nil return nil
} }
+11 -1
View File
@@ -22,6 +22,9 @@ final class AppSettings {
private enum UserDefaultsKeys: String { private enum UserDefaultsKeys: String {
case lastVersionLaunched case lastVersionLaunched
case seenInvites case seenInvites
case hasShownWelcomeScreen
case appLockNumberOfPINAttempts
case appLockNumberOfBiometricAttempts
case lastLoginDate case lastLoginDate
case migratedAccounts case migratedAccounts
case timelineStyle case timelineStyle
@@ -38,7 +41,6 @@ final class AppSettings {
case shouldCollapseRoomStateEvents case shouldCollapseRoomStateEvents
case userSuggestionsEnabled case userSuggestionsEnabled
case readReceiptsEnabled case readReceiptsEnabled
case hasShownWelcomeScreen
case swiftUITimelineEnabled case swiftUITimelineEnabled
case voiceMessageEnabled case voiceMessageEnabled
case mentionsEnabled case mentionsEnabled
@@ -122,10 +124,18 @@ final class AppSettings {
// MARK: - Security // MARK: - Security
/// The app must be locked with a PIN code as part of the authentication flow.
let appLockIsMandatory = false
/// The amount of time the app can remain in the background for without requesting the PIN/TouchID/FaceID. /// The amount of time the app can remain in the background for without requesting the PIN/TouchID/FaceID.
let appLockGracePeriod: TimeInterval = 180 let appLockGracePeriod: TimeInterval = 180
/// Any codes that the user isn't allowed to use for their PIN. /// Any codes that the user isn't allowed to use for their PIN.
let appLockPINCodeBlockList = ["0000", "1234"] let appLockPINCodeBlockList = ["0000", "1234"]
/// The number of attempts the user has made to unlock the app with a PIN code (resets when unlocked).
@UserPreference(key: UserDefaultsKeys.appLockNumberOfPINAttempts, defaultValue: 0, storageType: .userDefaults(store))
var appLockNumberOfPINAttempts: Int
/// The number of attempts the user has made to unlock the app with Touch/Face ID (resets when unlocked).
@UserPreference(key: UserDefaultsKeys.appLockNumberOfBiometricAttempts, defaultValue: 0, storageType: .userDefaults(store))
var appLockNumberOfBiometricAttempts: Int
// MARK: - Authentication // MARK: - Authentication
@@ -22,11 +22,14 @@ enum AppLockFlowCoordinatorAction: Equatable {
case lockApp case lockApp
/// Hide the unlock flow. /// Hide the unlock flow.
case unlockApp case unlockApp
/// Forces a logout of the user.
case forceLogout
} }
/// Coordinates the display of any screens shown when the app is locked. /// Coordinates the display of any screens shown when the app is locked.
class AppLockFlowCoordinator: CoordinatorProtocol { class AppLockFlowCoordinator: CoordinatorProtocol {
let appLockService: AppLockServiceProtocol let appLockService: AppLockServiceProtocol
let userIndicatorController: UserIndicatorController
let navigationCoordinator: NavigationRootCoordinator let navigationCoordinator: NavigationRootCoordinator
private var cancellables: Set<AnyCancellable> = [] private var cancellables: Set<AnyCancellable> = []
@@ -36,10 +39,18 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
init(appLockService: AppLockServiceProtocol, navigationCoordinator: NavigationRootCoordinator) { init(appLockService: AppLockServiceProtocol, userIndicatorController: UserIndicatorController, navigationCoordinator: NavigationRootCoordinator) {
self.appLockService = appLockService self.appLockService = appLockService
self.userIndicatorController = userIndicatorController
self.navigationCoordinator = navigationCoordinator self.navigationCoordinator = navigationCoordinator
appLockService.disabledPublisher
.sink { [weak self] _ in
// When the service is disabled via a force logout, we need to remove the activity indicator.
self?.userIndicatorController.retractAllIndicators()
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification) NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
.sink { [weak self] _ in .sink { [weak self] _ in
self?.applicationDidEnterBackground() self?.applicationDidEnterBackground()
@@ -54,7 +65,7 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
} }
func toPresentable() -> AnyView { func toPresentable() -> AnyView {
AnyView(navigationCoordinator.toPresentable()) AnyView(userIndicatorController.toPresentable())
} }
// MARK: - App unlock // MARK: - App unlock
@@ -91,6 +102,9 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
switch action { switch action {
case .appUnlocked: case .appUnlocked:
actionsSubject.send(.unlockApp) actionsSubject.send(.unlockApp)
case .forceLogout:
userIndicatorController.submitIndicator(UserIndicator(type: .modal, title: L10n.commonSigningOut, persistent: true))
actionsSubject.send(.forceLogout)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -0,0 +1,260 @@
//
// 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 Combine
import SwiftState
import SwiftUI
enum AppLockSetupFlowCoordinatorAction: Equatable {
case complete
}
/// Coordinates the display of any screens used to configure the App Lock feature.
class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {
private let presentingFlow: PresentationFlow
private let appLockService: AppLockServiceProtocol
private let navigationStackCoordinator: NavigationStackCoordinator
private let modalNavigationStackCoordinator = NavigationStackCoordinator()
/// The presentation context of the flow.
enum PresentationFlow {
/// The flow is shown as for mandatory PIN creation in the authentication flow
case authentication
/// The flow is shown from the Settings screen.
case settings
}
/// States the flow can find itself in
enum State: StateType {
/// The initial state, used before the flow starts
case initial
/// The unlock screen.
case unlock
/// The create PIN screen.
case createPIN
/// The allow biometrics screen.
case biometricsPrompt
/// The settings screen.
case settings
/// The flow is finished.
case complete
}
/// Events that can be triggered on the flow state machine
enum Event: EventType {
/// Start the flow.
case start
/// The user entered a PIN.
case pinEntered
/// The user completed the biometrics prompt.
case biometricsSet
/// The user wants to change their PIN.
case changePIN
/// The user wants to dismiss the flow.
case dismiss
}
private let stateMachine: StateMachine<State, Event>
private var cancellables: Set<AnyCancellable> = []
private let actionsSubject: PassthroughSubject<AppLockSetupFlowCoordinatorAction, Never> = .init()
var actions: AnyPublisher<AppLockSetupFlowCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(presentingFlow: PresentationFlow, appLockService: AppLockServiceProtocol, navigationStackCoordinator: NavigationStackCoordinator) {
self.presentingFlow = presentingFlow
self.appLockService = appLockService
self.navigationStackCoordinator = navigationStackCoordinator
stateMachine = .init(state: .initial)
configureStateMachine()
}
func start() {
stateMachine.tryEvent(.start)
}
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
// Deep links not supported.
}
func clearRoute(animated: Bool) {
// Deep links not supported.
}
// MARK: - Private
private func configureStateMachine() {
stateMachine.addRouteMapping { [weak self] event, fromState, _ in
guard let self else { return nil }
switch (event, fromState) {
case (.start, .initial):
if presentingFlow == .authentication { return .createPIN }
return appLockService.isEnabled ? .unlock : .createPIN
case (.pinEntered, .unlock):
return .settings
case (.pinEntered, .createPIN):
if presentingFlow == .authentication {
return appLockService.biometryType != .none ? .biometricsPrompt : .complete
} else {
return appLockService.biometricUnlockEnabled || appLockService.biometryType == .none ? .settings : .biometricsPrompt
}
case (.biometricsSet, .biometricsPrompt):
return presentingFlow == .settings ? .settings : .complete
case (.changePIN, .settings):
return .createPIN
case (.dismiss, _):
return .complete
default:
return nil
}
}
stateMachine.addAnyHandler(.any => .any) { [weak self] context in
guard let self else { return }
MXLog.info("Transitioning from `\(context.fromState)` to `\(context.toState)` with event `\(String(describing: context.event))`.")
switch (context.fromState, context.toState) {
case (.initial, .unlock):
showPINUnlock()
case (.initial, .createPIN):
showCreatePIN()
case (.unlock, .settings):
showSettings()
case (.createPIN, .biometricsPrompt):
showBiometricsPrompt()
case (.createPIN, .settings):
navigationStackCoordinator.setSheetCoordinator(nil)
case (.biometricsPrompt, .settings):
showSettings()
case (.settings, .createPIN):
showCreatePIN()
case (_, .complete):
complete(from: context.fromState)
default:
fatalError("Unhandled transition.")
}
}
stateMachine.addErrorHandler { context in
fatalError("Unexpected transition from `\(context.fromState)` to `\(context.toState)` with event `\(String(describing: context.event))`.")
}
}
private func showCreatePIN() {
// Despite appLockService.isMandatory existing, we don't use that here,
// to allow for cancellation when changing the PIN code within settings.
let isMandatory = presentingFlow == .authentication
let coordinator = AppLockSetupPINScreenCoordinator(parameters: .init(initialMode: .create,
isMandatory: isMandatory,
appLockService: appLockService))
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .cancel:
stateMachine.tryEvent(.dismiss)
case .complete:
stateMachine.tryEvent(.pinEntered)
}
}
.store(in: &cancellables)
if presentingFlow == .authentication {
navigationStackCoordinator.push(coordinator)
} else {
modalNavigationStackCoordinator.setRootCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(modalNavigationStackCoordinator)
}
}
private func showBiometricsPrompt() {
let coordinator = AppLockSetupBiometricsScreenCoordinator(parameters: .init(appLockService: appLockService))
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .continue:
stateMachine.tryEvent(.biometricsSet)
}
}
.store(in: &cancellables)
if presentingFlow == .authentication {
navigationStackCoordinator.push(coordinator)
} else {
modalNavigationStackCoordinator.push(coordinator)
}
}
private func showPINUnlock() {
let coordinator = AppLockSetupPINScreenCoordinator(parameters: .init(initialMode: .unlock,
isMandatory: false,
appLockService: appLockService))
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .cancel:
stateMachine.tryEvent(.dismiss)
case .complete:
stateMachine.tryEvent(.pinEntered)
}
}
.store(in: &cancellables)
modalNavigationStackCoordinator.setRootCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(modalNavigationStackCoordinator)
}
private func showSettings() {
let coordinator = AppLockSetupSettingsScreenCoordinator(parameters: .init(isMandatory: appLockService.isMandatory,
appLockService: appLockService))
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .changePINCode:
stateMachine.tryEvent(.changePIN)
case .appLockDisabled:
stateMachine.tryEvent(.dismiss)
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator, animated: false) { [weak self] in
self?.actionsSubject.send(.complete)
}
navigationStackCoordinator.setSheetCoordinator(nil)
}
/// Tear down the flow for completion.
private func complete(from state: State) {
switch state {
case .initial, .complete: fatalError()
case .unlock:
navigationStackCoordinator.setSheetCoordinator(nil)
actionsSubject.send(.complete)
case .createPIN:
navigationStackCoordinator.setSheetCoordinator(nil)
actionsSubject.send(.complete)
case .biometricsPrompt:
navigationStackCoordinator.setSheetCoordinator(nil)
actionsSubject.send(.complete)
case .settings:
navigationStackCoordinator.pop()
actionsSubject.send(.complete)
}
}
}
+6 -2
View File
@@ -328,6 +328,8 @@ public enum L10n {
public static var commonSettings: String { return L10n.tr("Localizable", "common_settings") } public static var commonSettings: String { return L10n.tr("Localizable", "common_settings") }
/// Shared location /// Shared location
public static var commonSharedLocation: String { return L10n.tr("Localizable", "common_shared_location") } public static var commonSharedLocation: String { return L10n.tr("Localizable", "common_shared_location") }
/// Signing out
public static var commonSigningOut: String { return L10n.tr("Localizable", "common_signing_out") }
/// Starting chat… /// Starting chat…
public static var commonStartingChat: String { return L10n.tr("Localizable", "common_starting_chat") } public static var commonStartingChat: String { return L10n.tr("Localizable", "common_starting_chat") }
/// Sticker /// Sticker
@@ -724,8 +726,10 @@ public enum L10n {
public static var screenAppLockSignoutAlertMessage: String { return L10n.tr("Localizable", "screen_app_lock_signout_alert_message") } public static var screenAppLockSignoutAlertMessage: String { return L10n.tr("Localizable", "screen_app_lock_signout_alert_message") }
/// You are being signed out /// You are being signed out
public static var screenAppLockSignoutAlertTitle: String { return L10n.tr("Localizable", "screen_app_lock_signout_alert_title") } public static var screenAppLockSignoutAlertTitle: String { return L10n.tr("Localizable", "screen_app_lock_signout_alert_title") }
/// You have 3 attempts to unlock /// Plural format key: "%#@COUNT@"
public static var screenAppLockSubtitle: String { return L10n.tr("Localizable", "screen_app_lock_subtitle") } public static func screenAppLockSubtitle(_ p1: Int) -> String {
return L10n.tr("Localizable", "screen_app_lock_subtitle", p1)
}
/// Plural format key: "%#@COUNT@" /// Plural format key: "%#@COUNT@"
public static func screenAppLockSubtitleWrongPin(_ p1: Int) -> String { public static func screenAppLockSubtitleWrongPin(_ p1: Int) -> String {
return L10n.tr("Localizable", "screen_app_lock_subtitle_wrong_pin", p1) return L10n.tr("Localizable", "screen_app_lock_subtitle_wrong_pin", p1)
@@ -117,6 +117,11 @@ class AnalyticsClientMock: AnalyticsClientProtocol {
} }
} }
class AppLockServiceMock: AppLockServiceProtocol { class AppLockServiceMock: AppLockServiceProtocol {
var isMandatory: Bool {
get { return underlyingIsMandatory }
set(value) { underlyingIsMandatory = value }
}
var underlyingIsMandatory: Bool!
var isEnabled: Bool { var isEnabled: Bool {
get { return underlyingIsEnabled } get { return underlyingIsEnabled }
set(value) { underlyingIsEnabled = value } set(value) { underlyingIsEnabled = value }
@@ -132,6 +137,21 @@ class AppLockServiceMock: AppLockServiceProtocol {
set(value) { underlyingBiometricUnlockEnabled = value } set(value) { underlyingBiometricUnlockEnabled = value }
} }
var underlyingBiometricUnlockEnabled: Bool! var underlyingBiometricUnlockEnabled: Bool!
var disabledPublisher: AnyPublisher<Void, Never> {
get { return underlyingDisabledPublisher }
set(value) { underlyingDisabledPublisher = value }
}
var underlyingDisabledPublisher: AnyPublisher<Void, Never>!
var numberOfPINAttempts: AnyPublisher<Int, Never> {
get { return underlyingNumberOfPINAttempts }
set(value) { underlyingNumberOfPINAttempts = value }
}
var underlyingNumberOfPINAttempts: AnyPublisher<Int, Never>!
var numberOfBiometricAttempts: AnyPublisher<Int, Never> {
get { return underlyingNumberOfBiometricAttempts }
set(value) { underlyingNumberOfBiometricAttempts = value }
}
var underlyingNumberOfBiometricAttempts: AnyPublisher<Int, Never>!
//MARK: - setupPINCode //MARK: - setupPINCode
@@ -25,6 +25,8 @@ struct AppLockScreenCoordinatorParameters {
enum AppLockScreenCoordinatorAction { enum AppLockScreenCoordinatorAction {
/// The user has successfully unlocked the app. /// The user has successfully unlocked the app.
case appUnlocked case appUnlocked
/// The user failed to unlock the app (or forgot their PIN).
case forceLogout
} }
final class AppLockScreenCoordinator: CoordinatorProtocol { final class AppLockScreenCoordinator: CoordinatorProtocol {
@@ -51,6 +53,8 @@ final class AppLockScreenCoordinator: CoordinatorProtocol {
switch action { switch action {
case .appUnlocked: case .appUnlocked:
self.actionsSubject.send(.appUnlocked) self.actionsSubject.send(.appUnlocked)
case .forceLogout:
self.actionsSubject.send(.forceLogout)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -19,6 +19,8 @@ import Foundation
enum AppLockScreenViewModelAction { enum AppLockScreenViewModelAction {
/// The user has successfully unlocked the app. /// The user has successfully unlocked the app.
case appUnlocked case appUnlocked
/// The user failed to unlock the app (or forgot their PIN).
case forceLogout
} }
struct AppLockScreenViewState: BindableState { struct AppLockScreenViewState: BindableState {
@@ -36,7 +38,7 @@ struct AppLockScreenViewState: BindableState {
/// The string shown in the screen's subtitle. /// The string shown in the screen's subtitle.
var subtitle: String { var subtitle: String {
if !isSubtitleWarning { if !isSubtitleWarning {
return L10n.screenAppLockSubtitle return L10n.screenAppLockSubtitle(maximumAttempts)
} else { } else {
return L10n.screenAppLockSubtitleWrongPin(maximumAttempts - numberOfPINAttempts) return L10n.screenAppLockSubtitleWrongPin(maximumAttempts - numberOfPINAttempts)
} }
@@ -50,8 +52,8 @@ struct AppLockScreenViewStateBindings {
} }
enum AppLockScreenAlertType { enum AppLockScreenAlertType {
/// The user has failed too many times, they're being signed out. /// The user has failed too many times, they're being logged out.
case forceSignOut case forcedLogout
/// The user has forgotten their PIN, confirm they're happy to sign out. /// The user has forgotten their PIN, confirm they're happy to sign out.
case confirmResetPIN case confirmResetPIN
} }
@@ -31,6 +31,12 @@ class AppLockScreenViewModel: AppLockScreenViewModelType, AppLockScreenViewModel
self.appLockService = appLockService self.appLockService = appLockService
super.init(initialViewState: AppLockScreenViewState(bindings: .init())) super.init(initialViewState: AppLockScreenViewState(bindings: .init()))
appLockService.numberOfPINAttempts
.weakAssign(to: \.state.numberOfPINAttempts, on: self)
.store(in: &cancellables)
showForceLogoutAlertIfNeeded()
} }
// MARK: - Public // MARK: - Public
@@ -58,24 +64,21 @@ class AppLockScreenViewModel: AppLockScreenViewModelType, AppLockScreenViewModel
state.bindings.alertInfo = .init(id: .confirmResetPIN, state.bindings.alertInfo = .init(id: .confirmResetPIN,
title: L10n.screenAppLockSignoutAlertTitle, title: L10n.screenAppLockSignoutAlertTitle,
message: L10n.screenAppLockSignoutAlertMessage, message: L10n.screenAppLockSignoutAlertMessage,
primaryButton: .init(title: L10n.actionOk, action: forceSignOut), primaryButton: .init(title: L10n.actionOk) { self.actionsSubject.send(.forceLogout) },
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
} }
private func handleInvalidPIN() { private func handleInvalidPIN() {
MXLog.warning("Invalid PIN code entered.") MXLog.warning("Invalid PIN code entered.")
state.numberOfPINAttempts += 1 showForceLogoutAlertIfNeeded()
}
if state.numberOfPINAttempts == 3 { private func showForceLogoutAlertIfNeeded() {
state.bindings.alertInfo = .init(id: .forceSignOut, if state.numberOfPINAttempts >= 3 {
state.bindings.alertInfo = .init(id: .forcedLogout,
title: L10n.screenAppLockSignoutAlertTitle, title: L10n.screenAppLockSignoutAlertTitle,
message: L10n.screenAppLockSignoutAlertMessage, message: L10n.screenAppLockSignoutAlertMessage,
primaryButton: .init(title: L10n.actionOk, action: nil)) primaryButton: .init(title: L10n.actionOk) { self.actionsSubject.send(.forceLogout) })
forceSignOut()
} }
} }
private func forceSignOut() {
// To be implemented.
}
} }
@@ -31,8 +31,10 @@ struct AppLockSetupBiometricsScreen: View {
.padding(.top, UIConstants.iconTopPaddingToNavigationBar) .padding(.top, UIConstants.iconTopPaddingToNavigationBar)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.toolbar(.visible, for: .navigationBar)
.background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.toolbar(.visible, for: .navigationBar)
.navigationBarBackButtonHidden()
.interactiveDismissDisabled()
.safeAreaInset(edge: .bottom) { .safeAreaInset(edge: .bottom) {
buttons buttons
.padding(.top, 16) .padding(.top, 16)
@@ -21,6 +21,8 @@ struct AppLockSetupPINScreenCoordinatorParameters {
/// Whether the screen should start in create or unlock mode. /// Whether the screen should start in create or unlock mode.
/// Specifying confirm here will raise a fatal error. /// Specifying confirm here will raise a fatal error.
let initialMode: AppLockSetupPINScreenMode let initialMode: AppLockSetupPINScreenMode
/// Whether the screen is mandatory or can be cancelled by the user.
let isMandatory: Bool
let appLockService: AppLockServiceProtocol let appLockService: AppLockServiceProtocol
} }
@@ -46,6 +48,7 @@ final class AppLockSetupPINScreenCoordinator: CoordinatorProtocol {
self.parameters = parameters self.parameters = parameters
viewModel = AppLockSetupPINScreenViewModel(initialMode: parameters.initialMode, viewModel = AppLockSetupPINScreenViewModel(initialMode: parameters.initialMode,
isMandatory: parameters.isMandatory,
appLockService: parameters.appLockService) appLockService: parameters.appLockService)
} }
@@ -56,9 +59,9 @@ final class AppLockSetupPINScreenCoordinator: CoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .complete: case .complete:
break actionsSubject.send(.complete)
case .cancel: case .cancel:
break actionsSubject.send(.cancel)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -35,6 +35,8 @@ enum AppLockSetupPINScreenMode {
struct AppLockSetupPINScreenViewState: BindableState { struct AppLockSetupPINScreenViewState: BindableState {
/// The current mode that the screen is in. /// The current mode that the screen is in.
var mode: AppLockSetupPINScreenMode var mode: AppLockSetupPINScreenMode
/// Whether the screen is mandatory or can be cancelled.
let isMandatory: Bool
var title: String { var title: String {
switch mode { switch mode {
@@ -30,9 +30,9 @@ class AppLockSetupPINScreenViewModel: AppLockSetupPINScreenViewModelType, AppLoc
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
init(initialMode: AppLockSetupPINScreenMode, appLockService: AppLockServiceProtocol) { init(initialMode: AppLockSetupPINScreenMode, isMandatory: Bool, appLockService: AppLockServiceProtocol) {
self.appLockService = appLockService self.appLockService = appLockService
super.init(initialViewState: AppLockSetupPINScreenViewState(mode: initialMode, bindings: .init(pinCode: ""))) super.init(initialViewState: AppLockSetupPINScreenViewState(mode: initialMode, isMandatory: isMandatory, bindings: .init(pinCode: "")))
} }
// MARK: - Public // MARK: - Public
@@ -21,6 +21,8 @@ import SwiftUI
struct AppLockSetupPINScreen: View { struct AppLockSetupPINScreen: View {
@ObservedObject var context: AppLockSetupPINScreenViewModel.Context @ObservedObject var context: AppLockSetupPINScreenViewModel.Context
@FocusState private var textFieldFocus
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: 48) { VStack(spacing: 48) {
@@ -28,6 +30,7 @@ struct AppLockSetupPINScreen: View {
PINTextField(pinCode: $context.pinCode, PINTextField(pinCode: $context.pinCode,
isSecure: context.viewState.mode == .unlock) isSecure: context.viewState.mode == .unlock)
.focused($textFieldFocus)
.onChange(of: context.pinCode) { newValue in .onChange(of: context.pinCode) { newValue in
guard newValue.count == 4 else { return } guard newValue.count == 4 else { return }
context.send(viewAction: .submitPINCode) context.send(viewAction: .submitPINCode)
@@ -38,6 +41,12 @@ struct AppLockSetupPINScreen: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.toolbar { toolbar }
.toolbar(.visible, for: .navigationBar)
.navigationBarBackButtonHidden()
.interactiveDismissDisabled(context.viewState.isMandatory)
.alert(item: $context.alertInfo)
.onAppear { textFieldFocus = true }
} }
var header: some View { var header: some View {
@@ -59,6 +68,17 @@ struct AppLockSetupPINScreen: View {
} }
} }
} }
@ToolbarContentBuilder
var toolbar: some ToolbarContent {
if !context.viewState.isMandatory {
ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
}
}
}
} }
// MARK: - Previews // MARK: - Previews
@@ -66,9 +86,15 @@ struct AppLockSetupPINScreen: View {
struct AppLockSetupPINScreen_Previews: PreviewProvider, TestablePreview { struct AppLockSetupPINScreen_Previews: PreviewProvider, TestablePreview {
static let service = AppLockService(keychainController: KeychainControllerMock(), static let service = AppLockService(keychainController: KeychainControllerMock(),
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
static let createViewModel = AppLockSetupPINScreenViewModel(initialMode: .create, appLockService: service) static let createViewModel = AppLockSetupPINScreenViewModel(initialMode: .create,
static let confirmViewModel = AppLockSetupPINScreenViewModel(initialMode: .confirm, appLockService: service) isMandatory: false,
static let unlockViewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, appLockService: service) appLockService: service)
static let confirmViewModel = AppLockSetupPINScreenViewModel(initialMode: .confirm,
isMandatory: false,
appLockService: service)
static let unlockViewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock,
isMandatory: false,
appLockService: service)
static var previews: some View { static var previews: some View {
NavigationStack { NavigationStack {
@@ -17,28 +17,31 @@
import Combine import Combine
import SwiftUI import SwiftUI
struct AppLockSettingsScreenCoordinatorParameters { struct AppLockSetupSettingsScreenCoordinatorParameters {
/// Whether App Lock is mandatory and can be disabled by the user.
let isMandatory: Bool
let appLockService: AppLockServiceProtocol let appLockService: AppLockServiceProtocol
} }
enum AppLockSettingsScreenCoordinatorAction { enum AppLockSetupSettingsScreenCoordinatorAction {
case done case changePINCode
case appLockDisabled
} }
final class AppLockSettingsScreenCoordinator: CoordinatorProtocol { final class AppLockSetupSettingsScreenCoordinator: CoordinatorProtocol {
private let parameters: AppLockSettingsScreenCoordinatorParameters private let parameters: AppLockSetupSettingsScreenCoordinatorParameters
private var viewModel: AppLockSettingsScreenViewModelProtocol private var viewModel: AppLockSetupSettingsScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<AppLockSettingsScreenCoordinatorAction, Never> = .init() private let actionsSubject: PassthroughSubject<AppLockSetupSettingsScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
var actions: AnyPublisher<AppLockSettingsScreenCoordinatorAction, Never> { var actions: AnyPublisher<AppLockSetupSettingsScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
init(parameters: AppLockSettingsScreenCoordinatorParameters) { init(parameters: AppLockSetupSettingsScreenCoordinatorParameters) {
self.parameters = parameters self.parameters = parameters
viewModel = AppLockSettingsScreenViewModel(appLockService: parameters.appLockService) viewModel = AppLockSetupSettingsScreenViewModel(appLockService: parameters.appLockService)
} }
func start() { func start() {
@@ -48,15 +51,15 @@ final class AppLockSettingsScreenCoordinator: CoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .changePINCode: case .changePINCode:
break actionsSubject.send(.changePINCode)
case .done: case .appLockDisabled:
self.actionsSubject.send(.done) actionsSubject.send(.appLockDisabled)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
func toPresentable() -> AnyView { func toPresentable() -> AnyView {
AnyView(AppLockSettingsScreen(context: viewModel.context)) AnyView(AppLockSetupSettingsScreen(context: viewModel.context))
} }
} }
@@ -17,36 +17,38 @@
import Foundation import Foundation
import LocalAuthentication import LocalAuthentication
enum AppLockSettingsScreenViewModelAction { enum AppLockSetupSettingsScreenViewModelAction {
/// The user would like to enter new PIN code. /// The user would like to enter new PIN code.
case changePINCode case changePINCode
case done /// The user has disabled the App Lock feature.
case appLockDisabled
} }
struct AppLockSettingsScreenViewState: BindableState { struct AppLockSetupSettingsScreenViewState: BindableState {
/// Whether App Lock is mandatory and can be disabled by the user.
let isMandatory: Bool
let biometryType: LABiometryType let biometryType: LABiometryType
var bindings: AppLockSettingsScreenViewStateBindings var bindings: AppLockSetupSettingsScreenViewStateBindings
var supportsBiometry: Bool { biometryType != .none } var supportsBiometry: Bool { biometryType != .none }
var enableBiometryTitle: String { L10n.screenAppLockSetupBiometricUnlockAllowTitle(biometryType.localizedString) } var enableBiometryTitle: String { L10n.screenAppLockSetupBiometricUnlockAllowTitle(biometryType.localizedString) }
} }
struct AppLockSettingsScreenViewStateBindings { struct AppLockSetupSettingsScreenViewStateBindings {
var enableBiometrics: Bool var enableBiometrics: Bool
var alertInfo: AlertInfo<AppLockSettingsScreenAlertType>? var alertInfo: AlertInfo<AppLockSetupSettingsScreenAlertType>?
} }
enum AppLockSettingsScreenAlertType { enum AppLockSetupSettingsScreenAlertType {
/// The alert shown to confirm the user would like to remove their PIN. /// The alert shown to confirm the user would like to remove their PIN.
case confirmRemovePINCode case confirmRemovePINCode
} }
enum AppLockSettingsScreenViewAction { enum AppLockSetupSettingsScreenViewAction {
/// The user would like to enter a new PIN code. /// The user would like to enter a new PIN code.
case changePINCode case changePINCode
/// The user would like to disable the App Lock feature. /// The user would like to disable the App Lock feature.
case disable case disable
/// The user has toggled the biometrics setting. /// The user has toggled the biometrics setting.
case enableBiometricsChanged case enableBiometricsChanged
case done
} }
@@ -17,25 +17,26 @@
import Combine import Combine
import SwiftUI import SwiftUI
typealias AppLockSettingsScreenViewModelType = StateStoreViewModel<AppLockSettingsScreenViewState, AppLockSettingsScreenViewAction> typealias AppLockSetupSettingsScreenViewModelType = StateStoreViewModel<AppLockSetupSettingsScreenViewState, AppLockSetupSettingsScreenViewAction>
class AppLockSettingsScreenViewModel: AppLockSettingsScreenViewModelType, AppLockSettingsScreenViewModelProtocol { class AppLockSetupSettingsScreenViewModel: AppLockSetupSettingsScreenViewModelType, AppLockSetupSettingsScreenViewModelProtocol {
private let appLockService: AppLockServiceProtocol private let appLockService: AppLockServiceProtocol
private var actionsSubject: PassthroughSubject<AppLockSettingsScreenViewModelAction, Never> = .init() private var actionsSubject: PassthroughSubject<AppLockSetupSettingsScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<AppLockSettingsScreenViewModelAction, Never> { var actions: AnyPublisher<AppLockSetupSettingsScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
init(appLockService: AppLockServiceProtocol) { init(appLockService: AppLockServiceProtocol) {
self.appLockService = appLockService self.appLockService = appLockService
super.init(initialViewState: AppLockSettingsScreenViewState(biometryType: appLockService.biometryType, super.init(initialViewState: AppLockSetupSettingsScreenViewState(isMandatory: appLockService.isMandatory,
bindings: .init(enableBiometrics: appLockService.biometricUnlockEnabled))) biometryType: appLockService.biometryType,
bindings: .init(enableBiometrics: appLockService.biometricUnlockEnabled)))
} }
// MARK: - Public // MARK: - Public
override func process(viewAction: AppLockSettingsScreenViewAction) { override func process(viewAction: AppLockSetupSettingsScreenViewAction) {
MXLog.info("View model: received view action: \(viewAction)") MXLog.info("View model: received view action: \(viewAction)")
switch viewAction { switch viewAction {
@@ -45,8 +46,6 @@ class AppLockSettingsScreenViewModel: AppLockSettingsScreenViewModelType, AppLoc
showRemovePINAlert() showRemovePINAlert()
case .enableBiometricsChanged: case .enableBiometricsChanged:
appLockService.biometricUnlockEnabled = state.bindings.enableBiometrics appLockService.biometricUnlockEnabled = state.bindings.enableBiometrics
case .done:
actionsSubject.send(.done)
} }
} }
@@ -64,6 +63,6 @@ class AppLockSettingsScreenViewModel: AppLockSettingsScreenViewModelType, AppLoc
/// Removes the user's PIN code, disabling the App Lock feature. /// Removes the user's PIN code, disabling the App Lock feature.
private func completeRemovePIN() { private func completeRemovePIN() {
appLockService.disable() appLockService.disable()
actionsSubject.send(.done) actionsSubject.send(.appLockDisabled)
} }
} }
@@ -17,7 +17,7 @@
import Combine import Combine
@MainActor @MainActor
protocol AppLockSettingsScreenViewModelProtocol { protocol AppLockSetupSettingsScreenViewModelProtocol {
var actions: AnyPublisher<AppLockSettingsScreenViewModelAction, Never> { get } var actions: AnyPublisher<AppLockSetupSettingsScreenViewModelAction, Never> { get }
var context: AppLockSettingsScreenViewModelType.Context { get } var context: AppLockSetupSettingsScreenViewModelType.Context { get }
} }
@@ -17,16 +17,19 @@
import Compound import Compound
import SwiftUI import SwiftUI
struct AppLockSettingsScreen: View { struct AppLockSetupSettingsScreen: View {
@ObservedObject var context: AppLockSettingsScreenViewModel.Context @ObservedObject var context: AppLockSetupSettingsScreenViewModel.Context
var body: some View { var body: some View {
Form { Form {
Section { Section {
ListRow(label: .plain(title: L10n.screenAppLockSettingsChangePin), ListRow(label: .plain(title: L10n.screenAppLockSettingsChangePin),
kind: .button { context.send(viewAction: .changePINCode) }) kind: .button { context.send(viewAction: .changePINCode) })
ListRow(label: .plain(title: L10n.screenAppLockSettingsRemovePin, role: .destructive),
kind: .button { context.send(viewAction: .disable) }) if !context.viewState.isMandatory {
ListRow(label: .plain(title: L10n.screenAppLockSettingsRemovePin, role: .destructive),
kind: .button { context.send(viewAction: .disable) })
}
} }
if context.viewState.supportsBiometry { if context.viewState.supportsBiometry {
@@ -48,25 +51,25 @@ struct AppLockSettingsScreen: View {
// MARK: - Previews // MARK: - Previews
struct AppLockSettingsScreen_Previews: PreviewProvider, TestablePreview { struct AppLockSetupSettingsScreen_Previews: PreviewProvider, TestablePreview {
static let faceIDViewModel = AppLockSettingsScreenViewModel(appLockService: AppLockServiceMock.mock(biometryType: .faceID)) static let faceIDViewModel = AppLockSetupSettingsScreenViewModel(appLockService: AppLockServiceMock.mock(biometryType: .faceID))
static let touchIDViewModel = AppLockSettingsScreenViewModel(appLockService: AppLockServiceMock.mock(biometryType: .touchID)) static let touchIDViewModel = AppLockSetupSettingsScreenViewModel(appLockService: AppLockServiceMock.mock(isMandatory: true, biometryType: .touchID))
static let biometricsUnavailableViewModel = AppLockSettingsScreenViewModel(appLockService: AppLockServiceMock.mock(biometryType: .none)) static let biometricsUnavailableViewModel = AppLockSetupSettingsScreenViewModel(appLockService: AppLockServiceMock.mock(biometryType: .none))
static var previews: some View { static var previews: some View {
NavigationStack { NavigationStack {
AppLockSettingsScreen(context: faceIDViewModel.context) AppLockSetupSettingsScreen(context: faceIDViewModel.context)
} }
.previewDisplayName("Face ID") .previewDisplayName("Face ID")
NavigationStack { NavigationStack {
AppLockSettingsScreen(context: touchIDViewModel.context) AppLockSetupSettingsScreen(context: touchIDViewModel.context)
} }
.previewDisplayName("Touch ID") .previewDisplayName("Touch ID (Mandatory)")
NavigationStack { NavigationStack {
AppLockSettingsScreen(context: biometricsUnavailableViewModel.context) AppLockSetupSettingsScreen(context: biometricsUnavailableViewModel.context)
} }
.previewDisplayName("No Biometrics") .previewDisplayName("PIN only")
} }
} }
@@ -29,10 +29,12 @@ class AuthenticationCoordinator: CoordinatorProtocol {
private let appSettings: AppSettings private let appSettings: AppSettings
private let analytics: AnalyticsService private let analytics: AnalyticsService
private let userIndicatorController: UserIndicatorControllerProtocol private let userIndicatorController: UserIndicatorControllerProtocol
private let appLockService: AppLockServiceProtocol
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var oidcPresenter: OIDCAuthenticationPresenter? private var oidcPresenter: OIDCAuthenticationPresenter?
private var appLockFlowCoordinator: AppLockSetupFlowCoordinator?
weak var delegate: AuthenticationCoordinatorDelegate? weak var delegate: AuthenticationCoordinatorDelegate?
@@ -40,12 +42,14 @@ class AuthenticationCoordinator: CoordinatorProtocol {
navigationStackCoordinator: NavigationStackCoordinator, navigationStackCoordinator: NavigationStackCoordinator,
appSettings: AppSettings, appSettings: AppSettings,
analytics: AnalyticsService, analytics: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol) { userIndicatorController: UserIndicatorControllerProtocol,
appLockService: AppLockServiceProtocol) {
self.authenticationService = authenticationService self.authenticationService = authenticationService
self.navigationStackCoordinator = navigationStackCoordinator self.navigationStackCoordinator = navigationStackCoordinator
self.appSettings = appSettings self.appSettings = appSettings
self.analytics = analytics self.analytics = analytics
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
self.appLockService = appLockService
} }
func start() { func start() {
@@ -237,25 +241,46 @@ class AuthenticationCoordinator: CoordinatorProtocol {
private func userHasSignedIn(userSession: UserSessionProtocol) { private func userHasSignedIn(userSession: UserSessionProtocol) {
appSettings.lastLoginDate = .now appSettings.lastLoginDate = .now
showAnalyticsPromptIfNeeded { [weak self] in if appSettings.appLockIsMandatory, !appLockService.isEnabled {
guard let self else { return } showAppLockSetupFlow(userSession: userSession)
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession) } else if analytics.shouldShowAnalyticsPrompt {
showAnalyticsPrompt(userSession: userSession)
} else {
delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
} }
} }
private func showAnalyticsPromptIfNeeded(completion: @escaping () -> Void) { private func showAppLockSetupFlow(userSession: UserSessionProtocol) {
guard analytics.shouldShowAnalyticsPrompt else { let coordinator = AppLockSetupFlowCoordinator(presentingFlow: .authentication,
completion() appLockService: appLockService,
return navigationStackCoordinator: navigationStackCoordinator)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .complete:
appLockFlowCoordinator = nil
if analytics.shouldShowAnalyticsPrompt {
showAnalyticsPrompt(userSession: userSession)
} else {
delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
}
}
} }
.store(in: &cancellables)
appLockFlowCoordinator = coordinator
coordinator.start()
}
private func showAnalyticsPrompt(userSession: UserSessionProtocol) {
let coordinator = AnalyticsPromptScreenCoordinator(analytics: analytics, termsURL: appSettings.analyticsConfiguration.termsURL) let coordinator = AnalyticsPromptScreenCoordinator(analytics: analytics, termsURL: appSettings.analyticsConfiguration.termsURL)
coordinator.actions coordinator.actions
.sink { action in .sink { [weak self] action in
guard let self else { return }
switch action { switch action {
case .done: case .done:
completion() delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -39,6 +39,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
private let parameters: SettingsScreenCoordinatorParameters private let parameters: SettingsScreenCoordinatorParameters
private var viewModel: SettingsScreenViewModelProtocol private var viewModel: SettingsScreenViewModelProtocol
private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator?
private let actionsSubject: PassthroughSubject<SettingsScreenCoordinatorAction, Never> = .init() private let actionsSubject: PassthroughSubject<SettingsScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
@@ -68,7 +70,7 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
case .analytics: case .analytics:
presentAnalyticsScreen() presentAnalyticsScreen()
case .appLock: case .appLock:
presentAppLockSettingsScreen() presentAppLockSetupFlow()
case .reportBug: case .reportBug:
presentBugReportScreen() presentBugReportScreen()
case .about: case .about:
@@ -146,9 +148,27 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
parameters.navigationStackCoordinator?.push(coordinator) parameters.navigationStackCoordinator?.push(coordinator)
} }
private func presentAppLockSettingsScreen() { private func presentAppLockSetupFlow() {
let coordinator = AppLockSettingsScreenCoordinator(parameters: .init(appLockService: parameters.appLockService)) guard let navigationStackCoordinator = parameters.navigationStackCoordinator else {
parameters.navigationStackCoordinator?.push(coordinator) MXLog.error("The navigation stack has gone! 🌫️")
return
}
let coordinator = AppLockSetupFlowCoordinator(presentingFlow: .settings,
appLockService: parameters.appLockService,
navigationStackCoordinator: navigationStackCoordinator)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .complete:
// The flow coordinator tidies up the stack, no need to do anything.
appLockSetupFlowCoordinator = nil
}
}
.store(in: &cancellables)
appLockSetupFlowCoordinator = coordinator
coordinator.start()
} }
private func presentBugReportScreen() { private func presentBugReportScreen() {
@@ -14,6 +14,7 @@
// limitations under the License. // limitations under the License.
// //
import Combine
import LocalAuthentication import LocalAuthentication
/// The service responsible for locking and unlocking the app. /// The service responsible for locking and unlocking the app.
@@ -24,6 +25,8 @@ class AppLockService: AppLockServiceProtocol {
private let timer: AppLockTimer private let timer: AppLockTimer
var isMandatory: Bool { appSettings.appLockIsMandatory }
var isEnabled: Bool { var isEnabled: Bool {
do { do {
guard appSettings.appLockFlowEnabled else { return false } guard appSettings.appLockFlowEnabled else { return false }
@@ -38,6 +41,12 @@ class AppLockService: AppLockServiceProtocol {
var biometryType: LABiometryType { context.biometryType } var biometryType: LABiometryType { context.biometryType }
var biometricUnlockEnabled = false // Needs to be stored, not sure if in the keychain or defaults yet. var biometricUnlockEnabled = false // Needs to be stored, not sure if in the keychain or defaults yet.
var numberOfPINAttempts: AnyPublisher<Int, Never> { appSettings.$appLockNumberOfPINAttempts }
var numberOfBiometricAttempts: AnyPublisher<Int, Never> { appSettings.$appLockNumberOfBiometricAttempts }
private var disabledSubject: PassthroughSubject<Void, Never> = .init()
var disabledPublisher: AnyPublisher<Void, Never> { disabledSubject.eraseToAnyPublisher() }
init(keychainController: KeychainControllerProtocol, appSettings: AppSettings) { init(keychainController: KeychainControllerProtocol, appSettings: AppSettings) {
self.keychainController = keychainController self.keychainController = keychainController
self.appSettings = appSettings self.appSettings = appSettings
@@ -68,6 +77,9 @@ class AppLockService: AppLockServiceProtocol {
func disable() { func disable() {
biometricUnlockEnabled = false biometricUnlockEnabled = false
keychainController.removePINCode() keychainController.removePINCode()
appSettings.appLockNumberOfPINAttempts = 0
appSettings.appLockNumberOfBiometricAttempts = 0
disabledSubject.send()
} }
func applicationDidEnterBackground() { func applicationDidEnterBackground() {
@@ -79,12 +91,20 @@ class AppLockService: AppLockServiceProtocol {
} }
func unlock(with pinCode: String) -> Bool { func unlock(with pinCode: String) -> Bool {
guard pinCode == keychainController.pinCode() else { return false } guard pinCode == keychainController.pinCode() else {
MXLog.warning("Wrong PIN entered.")
appSettings.appLockNumberOfPINAttempts += 1
return false
}
return completeUnlock() return completeUnlock()
} }
func unlockWithBiometrics() -> Bool { func unlockWithBiometrics() -> Bool {
guard biometryType != .none, biometricUnlockEnabled else { return false } guard biometryType != .none, biometricUnlockEnabled else {
MXLog.warning("\(biometryType) failed.")
appSettings.appLockNumberOfBiometricAttempts += 1
return false
}
return completeUnlock() return completeUnlock()
} }
@@ -103,6 +123,8 @@ class AppLockService: AppLockServiceProtocol {
/// Shared logic for completing an unlock via a PIN or biometry. /// Shared logic for completing an unlock via a PIN or biometry.
private func completeUnlock() -> Bool { private func completeUnlock() -> Bool {
timer.registerUnlock() timer.registerUnlock()
appSettings.appLockNumberOfPINAttempts = 0
appSettings.appLockNumberOfBiometricAttempts = 0
return true return true
} }
} }
@@ -14,6 +14,7 @@
// limitations under the License. // limitations under the License.
// //
import Combine
import LocalAuthentication import LocalAuthentication
enum AppLockServiceError: Error { enum AppLockServiceError: Error {
@@ -27,13 +28,19 @@ enum AppLockServiceError: Error {
@MainActor @MainActor
protocol AppLockServiceProtocol: AnyObject { protocol AppLockServiceProtocol: AnyObject {
/// The use of a PIN code is mandatory for this device.
var isMandatory: Bool { get }
/// The app has been configured to automatically lock with a PIN code. /// The app has been configured to automatically lock with a PIN code.
var isEnabled: Bool { get } var isEnabled: Bool { get }
/// The type of biometric authentication supported by the device. /// The type of biometric authentication supported by the device.
var biometryType: LABiometryType { get } var biometryType: LABiometryType { get }
/// Whether or not the user has enabled unlock via TouchID, FaceID or (possibly) OpticID. /// Whether or not the user has enabled unlock via TouchID, FaceID or (possibly) OpticID.
var biometricUnlockEnabled: Bool { get set } var biometricUnlockEnabled: Bool { get set }
/// A publisher that advertises when the service has been disabled.
var disabledPublisher: AnyPublisher<Void, Never> { get }
/// Sets the user's PIN code used to unlock the app. /// Sets the user's PIN code used to unlock the app.
func setupPINCode(_ pinCode: String) -> Result<Void, AppLockServiceError> func setupPINCode(_ pinCode: String) -> Result<Void, AppLockServiceError>
/// Validates the supplied PIN code is long enough, only contains digits and isn't a weak choice. /// Validates the supplied PIN code is long enough, only contains digits and isn't a weak choice.
@@ -50,15 +57,23 @@ protocol AppLockServiceProtocol: AnyObject {
func unlock(with pinCode: String) -> Bool func unlock(with pinCode: String) -> Bool
/// Attempt to unlock the app using FaceID or TouchID. /// Attempt to unlock the app using FaceID or TouchID.
func unlockWithBiometrics() -> Bool func unlockWithBiometrics() -> Bool
/// The number of attempts the user had made to unlock with a PIN code.
var numberOfPINAttempts: AnyPublisher<Int, Never> { get }
/// The number of attempts the user has made to unlock with Touch/Face ID.
var numberOfBiometricAttempts: AnyPublisher<Int, Never> { get }
} }
// sourcery: AutoMockable // sourcery: AutoMockable
extension AppLockServiceProtocol { } extension AppLockServiceProtocol { }
extension AppLockServiceMock { extension AppLockServiceMock {
static func mock(pinCode: String? = "2023", biometryType: LABiometryType = .faceID) -> AppLockServiceMock { static func mock(pinCode: String? = "2023", isMandatory: Bool = false, biometryType: LABiometryType = .faceID) -> AppLockServiceMock {
let mock = AppLockServiceMock() let mock = AppLockServiceMock()
mock.isEnabled = pinCode != nil mock.isEnabled = pinCode != nil
mock.isMandatory = isMandatory
mock.numberOfPINAttempts = PassthroughSubject<Int, Never>().eraseToAnyPublisher()
mock.numberOfBiometricAttempts = PassthroughSubject<Int, Never>().eraseToAnyPublisher()
mock.underlyingBiometryType = biometryType mock.underlyingBiometryType = biometryType
mock.underlyingBiometricUnlockEnabled = biometryType != .none mock.underlyingBiometricUnlockEnabled = biometryType != .none
mock.unlockWithClosure = { $0 == pinCode } mock.unlockWithClosure = { $0 == pinCode }
@@ -127,7 +127,8 @@ class MockScreen: Identifiable {
navigationStackCoordinator: navigationStackCoordinator, navigationStackCoordinator: navigationStackCoordinator,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics, analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController) userIndicatorController: ServiceLocator.shared.userIndicatorController,
appLockService: AppLockServiceMock())
retainedState.append(coordinator) retainedState.append(coordinator)
navigationStackCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator return navigationStackCoordinator
@@ -162,17 +163,12 @@ class MockScreen: Identifiable {
let coordinator = AppLockScreenCoordinator(parameters: .init(appLockService: appLockService)) let coordinator = AppLockScreenCoordinator(parameters: .init(appLockService: appLockService))
return coordinator return coordinator
case .appLockSetupFlow: case .appLockSetupFlow:
// Use the flow coordinator once more screens are added and remove the settings screen below.
let navigationStackCoordinator = NavigationStackCoordinator() let navigationStackCoordinator = NavigationStackCoordinator()
let appLockService = AppLockService(keychainController: KeychainControllerMock(), appSettings: ServiceLocator.shared.settings) let appLockService = AppLockService(keychainController: KeychainControllerMock(), appSettings: ServiceLocator.shared.settings)
let coordinator = AppLockSetupPINScreenCoordinator(parameters: .init(initialMode: .create, appLockService: appLockService)) let coordinator = AppLockSetupFlowCoordinator(presentingFlow: .settings,
navigationStackCoordinator.setRootCoordinator(coordinator) appLockService: appLockService,
return navigationStackCoordinator navigationStackCoordinator: navigationStackCoordinator)
case .appLockSettingsScreen: coordinator.start()
let navigationStackCoordinator = NavigationStackCoordinator()
let appLockService = AppLockServiceMock.mock(biometryType: .faceID)
let coordinator = AppLockSettingsScreenCoordinator(parameters: .init(appLockService: appLockService))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator return navigationStackCoordinator
case .home: case .home:
let navigationStackCoordinator = NavigationStackCoordinator() let navigationStackCoordinator = NavigationStackCoordinator()
@@ -31,7 +31,6 @@ enum UITestsScreenIdentifier: String {
case templateScreen case templateScreen
case appLockScreen case appLockScreen
case appLockSetupFlow case appLockSetupFlow
case appLockSettingsScreen
case home case home
case settings case settings
case bugReport case bugReport
@@ -1,26 +0,0 @@
//
// Copyright 2022 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 ElementX
import XCTest
@MainActor
class AppLockSettingsScreenUITests: XCTestCase {
func testScreen() async throws {
let app = Application.launch(.appLockSettingsScreen)
try await app.assertScreenshot(.appLockSettingsScreen)
}
}
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e09c1a718424e75c120fc2f2038f786388fcb2542231bc737c332e0b23004b33 oid sha256:02d0e19e329769fd507b7e02cbb95c55f36db403ba221b1362fea51e8e37a026
size 67281 size 100756
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3ebc5ae6771cf79d67f658af15893c271ac8181a4cc893ccac2c9ed96ff29d62
size 72971
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e4af88936877c1eb32530fe4eaaf0ec228f055a3c080766a15b81473752cdd53 oid sha256:bf66d4809566af646252cf16b7582ef3bbb56b55308041d264902de44ea91463
size 84142 size 164655
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:abb591b36f490253c34fd00092e45ba35961ba0aab4d6fcb8efb036ac3c2575e oid sha256:326cb365efab4ca083b39104402e1056ae915e4a230dacd07f494343e427cef7
size 67921 size 117772
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:908a80ad0ec73c875b8d581202903865d257ec23582fa3f3674c528569735aec
size 81618
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:47ed1ba1d4a0f067dc463b8e122c81e910b57ce580c6d66b5aa66b9ec5239452 oid sha256:1b0e2fe4e868bce1a4c758c8cc7026c17604bfe666c284ef83859ad0ac0aa001
size 91739 size 138452
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:89df727aac25c1d26ec86fe564ae97ca1687ba9d710dd5fc234f0052676c0a19 oid sha256:c5b462d8c9213706f06d4dab74a2b51bd29a5b961748ad3fd8156e48d407d832
size 69836 size 108960
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8eb2d44feaca1201363407063fcfd5356ed200030670533f43d4568da1d121b0
size 76181
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cb43f034b7fb94655b025e4ae9e6b652c27ebb6b4ab04fdae61c5958ab3d16c2 oid sha256:fee0ecd3529882e1799122602775c15cc0eb1f766885998b35cf078a06396640
size 101999 size 184338
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d08f08f1d3e22048432ec4733cd0192e763325760d338864e9395cb24e124f21 oid sha256:2d439b5c3e450115d6e46ae632ab3486a54763478db5e2111a763b9ab71da0cb
size 72314 size 127567
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:61fdc7d6975047d7c10592b8771484066e96705f28be0e1ec74ca7025e387fe7
size 83815
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d507ae7b47328770e3a82a730f00d9735a8d37da3df91818bcc1d76542b59325 oid sha256:01d584c4c83f2abded3b6d929cf4ef3781c18c1ee2a1d6d88174458a1b117e12
size 123595 size 168409
@@ -20,6 +20,7 @@ import XCTest
@MainActor @MainActor
class AppLockScreenViewModelTests: XCTestCase { class AppLockScreenViewModelTests: XCTestCase {
var appSettings: AppSettings!
var appLockService: AppLockService! var appLockService: AppLockService!
var keychainController: KeychainControllerMock! var keychainController: KeychainControllerMock!
var viewModel: AppLockScreenViewModelProtocol! var viewModel: AppLockScreenViewModelProtocol!
@@ -28,8 +29,9 @@ class AppLockScreenViewModelTests: XCTestCase {
override func setUp() { override func setUp() {
AppSettings.reset() AppSettings.reset()
appSettings = AppSettings()
keychainController = KeychainControllerMock() keychainController = KeychainControllerMock()
appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings()) appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings)
viewModel = AppLockScreenViewModel(appLockService: appLockService) viewModel = AppLockScreenViewModel(appLockService: appLockService)
} }
@@ -51,4 +53,60 @@ class AppLockScreenViewModelTests: XCTestCase {
// The app should become unlocked. // The app should become unlocked.
XCTAssertEqual(result, .appUnlocked) XCTAssertEqual(result, .appUnlocked)
} }
func testForgotPIN() {
// Given a fresh launch of the app.
XCTAssertNil(context.alertInfo, "No alert should be shown initially.")
// When the user has forgotten their PIN.
context.send(viewAction: .forgotPIN)
// Then an alert should be shown before logging out.
XCTAssertEqual(context.alertInfo?.id, .confirmResetPIN, "An alert should be shown before logging out.")
}
func testUnlockFailure() async throws {
// Given an invalid PIN code.
let pinCode = "2024"
keychainController.pinCodeReturnValue = "2023"
XCTAssertEqual(context.viewState.numberOfPINAttempts, 0, "The shouldn't be any attempts yet.")
XCTAssertFalse(context.viewState.isSubtitleWarning, "No warning should be shown yet.")
XCTAssertNil(context.alertInfo, "No alert should be shown yet.")
// When entering it on the lock screen.
viewModel.context.pinCode = pinCode
context.send(viewAction: .submitPINCode)
// Then a failed attempt should be shown.
XCTAssertEqual(context.viewState.numberOfPINAttempts, 1, "A failed attempt should have been recorded.")
XCTAssertTrue(context.viewState.isSubtitleWarning, "A warning should now be shown.")
XCTAssertNil(context.alertInfo, "No alert should be shown yet.")
// When entering twice more
context.send(viewAction: .submitPINCode)
context.send(viewAction: .submitPINCode)
// Then an alert should be shown
XCTAssertEqual(context.viewState.numberOfPINAttempts, 3, "All the attempts should have been recorded.")
XCTAssertTrue(context.viewState.isSubtitleWarning, "The warning should still be shown.")
XCTAssertEqual(context.alertInfo?.id, .forcedLogout, "An alert should now be shown.")
}
func testForceQuitRequiresLogout() {
// Given an app with a PIN set where the user attempted to unlock 3 times.
keychainController.pinCodeReturnValue = "2023"
appSettings.appLockNumberOfPINAttempts = 2
XCTAssertNil(context.alertInfo)
viewModel.context.pinCode = "0000"
context.send(viewAction: .submitPINCode)
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 3, "The app should have 3 failed attempts before the force quit.")
XCTAssertEqual(context.alertInfo?.id, .forcedLogout, "The app should be showing the alert before the force quit.")
// When force quitting the app and relaunching.
viewModel = nil
let freshViewModel = AppLockScreenViewModel(appLockService: appLockService)
// Then the alert should remain in place
XCTAssertEqual(freshViewModel.context.alertInfo?.id, .forcedLogout, "The new view model from the fresh launch should also show the alert")
}
} }
@@ -175,4 +175,47 @@ class AppLockServiceTests: XCTestCase {
XCTAssertFalse(service.isEnabled, "The service should no longer be enabled.") XCTAssertFalse(service.isEnabled, "The service should no longer be enabled.")
XCTAssertFalse(service.unlock(with: pinCode), "The initial PIN shouldn't work any more.") XCTAssertFalse(service.unlock(with: pinCode), "The initial PIN shouldn't work any more.")
} }
func testResetAttemptsOnUnlock() {
// Given a service that is enabled and has failed unlock attempts.
let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.")
return
}
appSettings.appLockNumberOfPINAttempts = 2
appSettings.appLockNumberOfBiometricAttempts = 2
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 2, "The initial conditions should be stored.")
XCTAssertEqual(appSettings.appLockNumberOfBiometricAttempts, 2, "The initial conditions should be stored.")
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
// When unlocking the service
XCTAssertTrue(service.unlock(with: pinCode), "The PIN should work.")
// Then the attempts counts should both be reset.
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 0, "The PIN attempts should be reset.")
XCTAssertEqual(appSettings.appLockNumberOfBiometricAttempts, 0, "The biometric attempts should be reset.")
}
func testResetAttemptsOnDisable() {
// Given a service that is enabled and has failed unlock attempts.
let pinCode = "2023"
guard case .success = service.setupPINCode(pinCode) else {
XCTFail("The PIN should be valid.")
return
}
appSettings.appLockNumberOfPINAttempts = 2
appSettings.appLockNumberOfBiometricAttempts = 2
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 2, "The initial conditions should be stored.")
XCTAssertEqual(appSettings.appLockNumberOfBiometricAttempts, 2, "The initial conditions should be stored.")
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
// When disabling the service
service.disable()
XCTAssertFalse(service.isEnabled, "The service should be disabled.")
// Then the attempts counts should both be reset.
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 0, "The PIN attempts should be reset.")
XCTAssertEqual(appSettings.appLockNumberOfBiometricAttempts, 0, "The biometric attempts should be reset.")
}
} }
@@ -19,12 +19,12 @@ import XCTest
@testable import ElementX @testable import ElementX
@MainActor @MainActor
class AppLockSettingsScreenViewModelTests: XCTestCase { class AppLockSetupSettingsScreenViewModelTests: XCTestCase {
var appLockService: AppLockServiceProtocol! var appLockService: AppLockServiceProtocol!
var keychainController: KeychainControllerMock! var keychainController: KeychainControllerMock!
var viewModel: AppLockSettingsScreenViewModelProtocol! var viewModel: AppLockSetupSettingsScreenViewModelProtocol!
var context: AppLockSettingsScreenViewModelType.Context { var context: AppLockSetupSettingsScreenViewModelType.Context {
viewModel.context viewModel.context
} }
@@ -36,7 +36,7 @@ class AppLockSettingsScreenViewModelTests: XCTestCase {
keychainController = KeychainControllerMock() keychainController = KeychainControllerMock()
appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings) appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings)
viewModel = AppLockSettingsScreenViewModel(appLockService: AppLockServiceMock.mock()) viewModel = AppLockSetupSettingsScreenViewModel(appLockService: AppLockServiceMock.mock())
} }
override func tearDown() { override func tearDown() {
@@ -37,7 +37,7 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
} }
func testCreatePIN() async throws { func testCreatePIN() async throws {
viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, appLockService: appLockService) viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, isMandatory: false, appLockService: appLockService)
XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.") XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.")
let createDeferred = deferFulfillment(context.$viewState, message: "A valid PIN needs confirming.") { $0.mode == .confirm } let createDeferred = deferFulfillment(context.$viewState, message: "A valid PIN needs confirming.") { $0.mode == .confirm }
@@ -53,7 +53,7 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
} }
func testCreateWeakPIN() async throws { func testCreateWeakPIN() async throws {
viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, appLockService: appLockService) viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, isMandatory: false, appLockService: appLockService)
XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.") XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.")
XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.") XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.")
@@ -65,7 +65,7 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
} }
func testCreatePINMismatch() async throws { func testCreatePINMismatch() async throws {
viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, appLockService: appLockService) viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, isMandatory: false, appLockService: appLockService)
XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.") XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.")
XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.") XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.")
@@ -83,7 +83,7 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
} }
func testUnlock() async throws { func testUnlock() async throws {
viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, appLockService: appLockService) viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, isMandatory: false, appLockService: appLockService)
let pinCode = "2023" let pinCode = "2023"
keychainController.pinCodeReturnValue = pinCode keychainController.pinCodeReturnValue = pinCode
keychainController.containsPINCodeReturnValue = true keychainController.containsPINCodeReturnValue = true
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c78cada10d7c15137a635b0e841fe20e124814921d953296fca5ed16dea93fef oid sha256:3f1fa2ac91c3c5966db4e7cf34e8f4d724547d583147ac23bd392c536b0357bb
size 98997 size 105733
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f2aa8303cb959ebeae8a6b6bced04f2c3ecdfe429c97f153a38a2d650dddadf4 oid sha256:a3de8520c297d0c53f500a5cbe1322d50ae1f76d7c4678ab7c7e1d243c6a8657
size 99386 size 106000
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e53aa446554e19bc47b31f59038ec3caeeaba8c4c15a1884f5e510f30eac4c61 oid sha256:641c74d77ed0b43113fda06653a3a7eda92906a81a682cbd6b781aabcce51ab0
size 72867 size 76727
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:905d8d16cfce1540a91a3c8ef7d4295b844d2aab4c3ed3035230277acb56e228
size 94357
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f918886d8da2987f6ae26762267557ce2f7d7f0b02ce985de56caa3fb7ee305c
size 83101
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08af67d397dafe03f60b36e2d1650a9031c12041ce5e015f6c2f81af4e9731fa
size 88510
+1
View File
@@ -0,0 +1 @@
Add an AppLockSetupFlowCoordinator for creating a PIN with both mandatory and optional flows.