diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 67287a142c..44c9e94590 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070FD43DC6BF4E50217965A /* LocalizationTests.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 */; }; 020F7E70167FB2833266F2F0 /* AnalyticsSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39D7F513A36C9C1951DB44C /* AnalyticsSettingsScreen.swift */; }; 024E70451A7CD9E4E034D8A9 /* VoiceMessageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B976F8B2AA654D923422 /* VoiceMessageRoomTimelineItem.swift */; }; @@ -27,6 +28,7 @@ 06AA515C7053FD7E17A5CF81 /* RoomNotificationSettingsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66901977F6469D03C333DF32 /* RoomNotificationSettingsScreenUITests.swift */; }; 06B55882911B4BF5B14E9851 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */; }; + 06F8EDF52E33A2D36BCC1161 /* AppLockScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6F88FE35A0979D2821E06 /* AppLockScreen.swift */; }; 071A017E415AD378F2961B11 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */; }; 07CC13C5729C24255348CBBD /* ElementCallWidgetDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */; }; @@ -41,6 +43,7 @@ 0A194F5E70B5A628C1BF4476 /* AdvancedSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */; }; 0AA0477E063E72B786A983CF /* AnalyticsPromptScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E1FF2DA52B1DE7CAEC5422 /* AnalyticsPromptScreenViewModel.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 */; }; 0B57C2399B9E1CE5CE0D8005 /* ComposerToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D121B4FCFC38DBCC17BCC6D6 /* ComposerToolbar.swift */; }; 0BDA19079FD6E17C5AC62E22 /* RoomDetailsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */; }; @@ -56,7 +59,6 @@ 0E08BB72B2258652CF501A8B /* Prefire in Frameworks */ = {isa = PBXBuildFile; productRef = 9B68DE8678BF67D4612BCC16 /* Prefire */; }; 0E8C480700870BB34A2A360F /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4003BC24B24C9E63D3304177 /* DeviceKit */; }; 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; - 0EAEA507586717B055441970 /* AppLockScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C80AD634BF0A1767FE8941C5 /* AppLockScreenCoordinator.swift */; }; 0ED691ADC9C2EA457E7A9427 /* FormattingToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE449DFBA7CC863EEB2FD2A /* FormattingToolbar.swift */; }; 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; 0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */; }; @@ -154,6 +156,7 @@ 2CA6ABBC9A88EB89EA52FCCB /* ConfettiScene.scn in Resources */ = {isa = PBXBuildFile; fileRef = B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */; }; 2DA27D78560D5F79B917E163 /* AudioConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */; }; 2DA90E38FF4E696825810C1A /* WaitlistScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB08484CD5D77C9BF97AA78 /* WaitlistScreenUITests.swift */; }; + 2DD9D0FE7CB5CFC80D071451 /* AppLockScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */; }; 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; }; 2E8C6672D0EE7D5B1BEDB8E2 /* ServerConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7478623CECC9438014244BA /* ServerConfirmationScreen.swift */; }; 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086B997409328F091EBA43CE /* RoomScreenUITests.swift */; }; @@ -168,7 +171,6 @@ 3113065AABBC14CEAE6843FA /* UserSessionFlowCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8774CF614849664B5B3C2A1 /* UserSessionFlowCoordinatorStateMachine.swift */; }; 3116693C5EB476E028990416 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74611A4182DCF5F4D42696EC /* XCTestCase.swift */; }; 32B7891D937377A59606EDFC /* UserFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DD8599815136EFF5B73F38 /* UserFlowTests.swift */; }; - 33094DB91C3A4131E76B2C07 /* AppLockScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5574FD6FC3C2DC0DF160A85 /* AppLockScreenViewModelProtocol.swift */; }; 339BC18777912E1989F2F17D /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584A61D9C459FAFEF038A7C0 /* Section.swift */; }; 33CA777C9DF263582D77A67F /* VoiceMessagePreviewComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC9F57320EC80C7CE34FE4A /* VoiceMessagePreviewComposer.swift */; }; 33CAC1226DFB8B5D8447D286 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; }; @@ -182,6 +184,7 @@ 352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */; }; 355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */; }; 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 */; }; 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; }; 36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; }; @@ -255,7 +258,6 @@ 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */; }; 4A618590DEB72C4F186BFED4 /* UserSessionFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */; }; 4A85928E27D4C1A548A06EE9 /* StartChatScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 052B2F924572AFD70B5F500E /* StartChatScreenViewModel.swift */; }; - 4A945B96B87D61F873F48933 /* AppLockScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC930E5F7F138112CAE5AC63 /* AppLockScreenModels.swift */; }; 4AAA8606FBA290E23D15422E /* AvatarHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC743C7A85E3171BCBF0A653 /* AvatarHeaderView.swift */; }; 4B978C09567387EF4366BD7A /* MediaLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EF1AC723C2609C7705569CA /* MediaLoaderTests.swift */; }; 4BAB8222DBA0B4207D1223E0 /* NotificationSettingsProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */; }; @@ -329,6 +331,7 @@ 6189B4ABD535CE526FA1107B /* StartChatViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */; }; 61941DEE5F3834765770BE01 /* InviteUsersScreenSelectedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F32E0B4B83D2A11EE8D011 /* InviteUsersScreenSelectedItem.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 */; }; 62418EA4E3EB597AD184AEB6 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; }; 62910B515BCB4B455E24D7C1 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */; }; @@ -341,7 +344,6 @@ 64AB99285DC4437C0DDE9585 /* MenuSheetLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49ABAB186CF00B15C5521D04 /* MenuSheetLabelStyle.swift */; }; 64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */; }; 64D05250CEDE8B604119F6E6 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981663D961C94270FA035FD0 /* Alert.swift */; }; - 64DD8AB9CA0405A43043BDF8 /* AppLockScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891D46CF94626F05614829A2 /* AppLockScreen.swift */; }; 64F43D7390DA2A0AFD6BA911 /* VideoRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */; }; 64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */; }; 651341E67C3514F9811A1EC1 /* LoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F598B1B346DAF223651C91 /* LoginScreenCoordinator.swift */; }; @@ -393,6 +395,7 @@ 71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; }; 7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; }; 7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */; }; + 73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */; }; 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; }; 743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */; }; 744114780862F0BD1A2D57D6 /* CreatePollScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB23BEAF8831DC6A57E39F52 /* CreatePollScreenCoordinator.swift */; }; @@ -451,6 +454,7 @@ 829062DD3C3F7016FE1A6476 /* RoomDetailsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */; }; 8317E1314C00DCCC99D30DA8 /* TextBasedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9227F7495DA43324050A863 /* TextBasedRoomTimelineItem.swift */; }; 83A4DAB181C56987C3E804FF /* MapTilerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */; }; + 84215E902C62E9B8E8AB79F0 /* AppLockSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267C0279BB8D907E2C40DDCA /* AppLockSettingsScreenCoordinator.swift */; }; 8421FFCD5360A15D170922A8 /* ProgressMaskModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79A1D75C7C52CD14A327CC90 /* ProgressMaskModifier.swift */; }; 84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; }; 8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */; }; @@ -529,9 +533,11 @@ 968A5B890004526AB58A217C /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; 97189E495F0E47805D1868DB /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 527578916BD388A09F5A8036 /* DTCoreText */; }; 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; }; + 97969EF0B9C412CD38E5CA93 /* AppLockScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4005D82E9D27BAF006A8FE1 /* AppLockScreenViewModel.swift */; }; 981853650217B6C8ECDD998C /* NavigationRootCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */; }; 983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */; }; 988BA75A182738150894A23F /* UserIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8AE4B3273BA189FDCD4055C /* UserIndicator.swift */; }; + 9912F9EB2D6589141A2957B4 /* AppLockScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C44BBC892499BE45B074F89 /* AppLockScreenCoordinator.swift */; }; 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; 9965CB800CE6BC74ACA969FC /* EncryptedHistoryRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75697AB5E64A12F1F069F511 /* EncryptedHistoryRoomTimelineView.swift */; }; 99ED42B8F8D6BFB1DBCF4C45 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = D661CAB418C075A94306A792 /* AnalyticsEvents */; }; @@ -593,6 +599,7 @@ A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; }; A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.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 */; }; A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; }; A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; }; @@ -680,7 +687,6 @@ BD782053BE4C3D2F0BDE5699 /* ServiceLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */; }; BDA68E8D95B2B24B28825B8B /* LoginScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C368CAB3063EF275357ECD4 /* LoginScreenViewModel.swift */; }; BDED6DA7AD1E76018C424143 /* LegalInformationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C34667458773B02AB5FB0B2 /* LegalInformationScreenViewModel.swift */; }; - BE641CE5F9036B9AD7367DF1 /* AppLockScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1CBFD0D6D5AA0C8DCA0DA6 /* AppLockScreenViewModel.swift */; }; BEA646DF302711A753F0D420 /* MapTilerStyleBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225EFCA26877E75CDFE7F48D /* MapTilerStyleBuilderProtocol.swift */; }; BF675964C9159F718589C36A /* AnalyticsSettingsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.swift */; }; C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */; }; @@ -749,6 +755,7 @@ D181AC8FF236B7F91C0A8C28 /* MapTiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AA3F4B285570805CB0CCDD /* MapTiler.swift */; }; D19A748E95E2FAB2940570F0 /* CallScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4103AB4340F2974D690A12A /* CallScreen.swift */; }; D1EEF0CB0F5D9C15E224E670 /* landscape_test_video.mov in Resources */ = {isa = PBXBuildFile; fileRef = 9A2AC7BE17C05CF7D2A22338 /* landscape_test_video.mov */; }; + D2048FD56760BDABA3DB5FC2 /* AppLockServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */; }; D2A15D03F81342A09340BD56 /* AnalyticsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFEEE93B82937B2E86F92EB /* AnalyticsScreen.swift */; }; D2D70B5DB1A5E4AF0CD88330 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 033DB41C51865A2E83174E87 /* target.yml */; }; D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */; }; @@ -818,6 +825,7 @@ E77469C5CD7F7F58C0AC9752 /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 3FFDA99C98BE05F43A92343B /* test_pdf.pdf */; }; E78D429F18071545BF661A52 /* RoomDetailsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */; }; E794AB6ABE1FF5AF0573FEA1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332DFE9642F0A46ECA0497B /* BlurHashEncode.swift */; }; + E79D79CDAFE8BEBCC3AECA54 /* AppLockScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */; }; E89536FC8C0E4B79E9842A78 /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0197EAE9D45A662B8847B6 /* RoomTimelineControllerProtocol.swift */; }; E9347F56CF0683208F4D9249 /* RoomNotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A9B5225D0881CEFA2CF7C9 /* RoomNotificationSettingsScreenViewModel.swift */; }; E9560744F7B0292E20ECE5F2 /* RoomDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */; }; @@ -978,6 +986,7 @@ 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsChatType.swift; sourceTree = ""; }; 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = ""; }; 07E65E613F057697A1A0BC03 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; + 08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelProtocol.swift; sourceTree = ""; }; 086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = ""; }; 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentViewModelTests.swift; sourceTree = ""; }; 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProviderProtocol.swift; sourceTree = ""; }; @@ -1081,6 +1090,8 @@ 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = ""; }; 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = ""; }; 260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = ""; }; + 267C0279BB8D907E2C40DDCA /* AppLockSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSettingsScreenCoordinator.swift; sourceTree = ""; }; + 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceProtocol.swift; sourceTree = ""; }; 2757B1BE23DF8AA239937243 /* AudioConverterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverterProtocol.swift; sourceTree = ""; }; 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfigurationScreenViewStateTests.swift; sourceTree = ""; }; 27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; @@ -1117,6 +1128,7 @@ 3203C6566DC17B7AECC1B7FD /* RoomNotificationSettingsUserDefinedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsUserDefinedScreen.swift; sourceTree = ""; }; 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyProtocol.swift; sourceTree = ""; }; 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelTests.swift; sourceTree = ""; }; + 33284693F54382F46CFD2EDD /* AppLockSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; 33649299575BADC34924ABC6 /* InvitesScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenCoordinator.swift; sourceTree = ""; }; 33720F7AD25E85E4A84669E8 /* MapTilerGeocoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerGeocoding.swift; sourceTree = ""; }; 33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; @@ -1144,6 +1156,7 @@ 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenUITests.swift; sourceTree = ""; }; 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCategory.swift; sourceTree = ""; }; 3C368CAB3063EF275357ECD4 /* LoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModel.swift; sourceTree = ""; }; + 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenModels.swift; sourceTree = ""; }; 3CCD41CD67DB5DA0D436BFE9 /* VoiceMessageRoomPlaybackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomPlaybackView.swift; sourceTree = ""; }; 3CCE3636E3D01477C8B2E9D0 /* ReportContentScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenModels.swift; sourceTree = ""; }; 3CFD5EB0B0EEA4549FB49784 /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; @@ -1237,6 +1250,7 @@ 5644919DB2022397D9D5825A /* MockSoftLogoutScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSoftLogoutScreenState.swift; sourceTree = ""; }; 565F1B2B300597C616B37888 /* FullscreenDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenDialog.swift; sourceTree = ""; }; 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = ""; }; + 56D6F88FE35A0979D2821E06 /* AppLockScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreen.swift; sourceTree = ""; }; 57916A1578D8043BB0795441 /* GeneratedMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedMocks.swift; sourceTree = ""; }; 57B6B383F1FD04CC0E7B60C6 /* AnalyticsConsentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsConsentState.swift; sourceTree = ""; }; 57EAAF82432B0B53881CF826 /* AudioRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineItem.swift; sourceTree = ""; }; @@ -1259,6 +1273,7 @@ 5D9A987EAA44E2E1BDCDAFDC /* EstimatedWaveformView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimatedWaveformView.swift; sourceTree = ""; }; 5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProperties.swift; sourceTree = ""; }; 5EB2CAA266B921D128C35710 /* LegalInformationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenCoordinator.swift; sourceTree = ""; }; + 5F088B61525099A48909743B /* AppLockSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSettingsScreenUITests.swift; sourceTree = ""; }; 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = ""; }; 5F8002D0392A476D2758B291 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = ""; }; 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyProtocol.swift; sourceTree = ""; }; @@ -1277,6 +1292,7 @@ 6569593FA36B22259E806A67 /* AudioRecorderState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderState.swift; sourceTree = ""; }; 65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryService.swift; sourceTree = ""; }; 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationServiceProxy.swift; sourceTree = ""; }; + 6615CBDE154455007F456DBB /* AppLockSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSettingsScreen.swift; sourceTree = ""; }; 6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = ""; }; 667DD3A9D932D7D9EB380CAA /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sk; path = sk.lproj/Localizable.stringsdict; sourceTree = ""; }; 66901977F6469D03C333DF32 /* RoomNotificationSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenUITests.swift; sourceTree = ""; }; @@ -1381,9 +1397,9 @@ 8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignedScrollView.swift; sourceTree = ""; }; 8896CDD20CA2D87EA3B848A1 /* RoomNotificationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreen.swift; sourceTree = ""; }; 889DEDD63C68ABDA8AD29812 /* VoiceMessageMediaManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManagerProtocol.swift; sourceTree = ""; }; - 891D46CF94626F05614829A2 /* AppLockScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreen.swift; sourceTree = ""; }; 89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerStateTests.swift; sourceTree = ""; }; 892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = ""; }; + 892EF45CCC5D2BF0FD1F770C /* AppLockSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSettingsScreenViewModel.swift; sourceTree = ""; }; 893777A4997BBDB68079D4F5 /* ArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayTests.swift; sourceTree = ""; }; 8977176AB534AA41630395BC /* LegalInformationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModelProtocol.swift; sourceTree = ""; }; 897DF5E9A70CE05A632FC8AF /* UTType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTType.swift; sourceTree = ""; }; @@ -1391,6 +1407,7 @@ 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainMentionBuilder.swift; sourceTree = ""; }; 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoader.swift; sourceTree = ""; }; 8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenModels.swift; sourceTree = ""; }; + 8C44BBC892499BE45B074F89 /* AppLockScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenCoordinator.swift; sourceTree = ""; }; 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItemContent.swift; sourceTree = ""; }; 8CC23C63849452BC86EA2852 /* ButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyle.swift; sourceTree = ""; }; 8D168471461717AF5689F64B /* OnboardingScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreenUITests.swift; sourceTree = ""; }; @@ -1506,6 +1523,7 @@ AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptUITests.swift; sourceTree = ""; }; AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderTests.swift; sourceTree = ""; }; B0A307A44F952CD73E63AE31 /* RoomEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomEventStringBuilder.swift; sourceTree = ""; }; + B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSettingsScreenViewModelTests.swift; sourceTree = ""; }; B16048D30F0438731C41F775 /* StateRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineItem.swift; sourceTree = ""; }; B16CAF20C9AC874A210E2DCF /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = ""; }; B1E227F34BE43B08E098796E /* TestablePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestablePreview.swift; sourceTree = ""; }; @@ -1515,6 +1533,7 @@ B3005886F00029F058DB62BE /* StartChatScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenCoordinator.swift; sourceTree = ""; }; B383DCD3DCB19E00FD478A5F /* ConfirmationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationDialog.swift; sourceTree = ""; }; B3A1398EFF65090FDA1CB639 /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = ""; }; + B4005D82E9D27BAF006A8FE1 /* AppLockScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModel.swift; sourceTree = ""; }; B43456E73F8A2D52B69B9FB9 /* TemplateScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModel.swift; sourceTree = ""; }; B48B7AD4908C5C374517B892 /* MapAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MapAssets.xcassets; sourceTree = ""; }; B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+SwiftUI.swift"; sourceTree = ""; }; @@ -1533,6 +1552,7 @@ B81B6170DB690013CEB646F4 /* MapLibreModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreModels.swift; sourceTree = ""; }; B83BC0DC9A2DF2DD60F9B6E9 /* NotificationSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenUITests.swift; sourceTree = ""; }; B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = ""; }; + B86F43BF1C26CB31B4BFA610 /* AppLockSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSettingsScreenModels.swift; sourceTree = ""; }; B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = ""; }; B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = ""; }; B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; @@ -1546,9 +1566,7 @@ BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = ""; }; BBEC57C204D77908E355EF42 /* AudioRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderProtocol.swift; sourceTree = ""; }; BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = ""; }; - BC930E5F7F138112CAE5AC63 /* AppLockScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenModels.swift; sourceTree = ""; }; BCF54536699ACEE3DB6BA3CB /* CompletionSuggestionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionService.swift; sourceTree = ""; }; - BD1CBFD0D6D5AA0C8DCA0DA6 /* AppLockScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModel.swift; sourceTree = ""; }; BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationContent.swift; sourceTree = ""; }; BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemReplyDetails.swift; sourceTree = ""; }; BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenCoordinator.swift; sourceTree = ""; }; @@ -1590,7 +1608,6 @@ C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomList.swift; sourceTree = ""; }; C796FC1DFDBCDD5573D0360F /* WaitlistScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelTests.swift; sourceTree = ""; }; - C80AD634BF0A1767FE8941C5 /* AppLockScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenCoordinator.swift; sourceTree = ""; }; C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = ""; }; C833673B334A0651AB46F30B /* StaticLocationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModelTests.swift; sourceTree = ""; }; C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineView.swift; sourceTree = ""; }; @@ -1691,7 +1708,6 @@ E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteURLParserTests.swift; sourceTree = ""; }; E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineView.swift; sourceTree = ""; }; E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; - E5574FD6FC3C2DC0DF160A85 /* AppLockScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelProtocol.swift; sourceTree = ""; }; E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyProtocol.swift; sourceTree = ""; }; E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; E5F2B6443D1ED8602F328539 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1886,6 +1902,14 @@ path = VoiceMessage; sourceTree = ""; }; + 01FACE97F4C9E9F522D401EB /* View */ = { + isa = PBXGroup; + children = ( + 6615CBDE154455007F456DBB /* AppLockSettingsScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 0210F4932B59277E2EEEF7BC /* RoomNotificationSettingsScreen */ = { isa = PBXGroup; children = ( @@ -2062,6 +2086,15 @@ path = CallScreen; sourceTree = ""; }; + 13263FFEA7749D822B51AA90 /* AppLock */ = { + isa = PBXGroup; + children = ( + 3AD37D7DDF9904587601239D /* AppLockScreen */, + CE39C9B97963CC30AB0859E5 /* AppLockSettingsScreen */, + ); + path = AppLock; + sourceTree = ""; + }; 13ACE3300D6A86770E757FC0 /* View */ = { isa = PBXGroup; children = ( @@ -2392,6 +2425,18 @@ path = VoiceMessages; sourceTree = ""; }; + 3AD37D7DDF9904587601239D /* AppLockScreen */ = { + isa = PBXGroup; + children = ( + 8C44BBC892499BE45B074F89 /* AppLockScreenCoordinator.swift */, + 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */, + B4005D82E9D27BAF006A8FE1 /* AppLockScreenViewModel.swift */, + 08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */, + F49710373735A3B8BB76DFF7 /* View */, + ); + path = AppLockScreen; + sourceTree = ""; + }; 3D22B0A4FC9008F7E353D0EA /* View */ = { isa = PBXGroup; children = ( @@ -2556,14 +2601,6 @@ path = View; sourceTree = ""; }; - 4AEBBDDC75A39318FFF01EBF /* View */ = { - isa = PBXGroup; - children = ( - 891D46CF94626F05614829A2 /* AppLockScreen.swift */, - ); - path = View; - sourceTree = ""; - }; 4B5DC42A1DB20ECEB0FF67CB /* Tests */ = { isa = PBXGroup; children = ( @@ -3035,22 +3072,11 @@ path = Items; sourceTree = ""; }; - 77566988A0A4F94744C3818B /* AppLockScreen */ = { - isa = PBXGroup; - children = ( - C80AD634BF0A1767FE8941C5 /* AppLockScreenCoordinator.swift */, - BC930E5F7F138112CAE5AC63 /* AppLockScreenModels.swift */, - BD1CBFD0D6D5AA0C8DCA0DA6 /* AppLockScreenViewModel.swift */, - E5574FD6FC3C2DC0DF160A85 /* AppLockScreenViewModelProtocol.swift */, - 4AEBBDDC75A39318FFF01EBF /* View */, - ); - path = AppLockScreen; - sourceTree = ""; - }; 7803E03F759061C948D66B7E /* AppLock */ = { isa = PBXGroup; children = ( 851B95BB98649B8E773D6790 /* AppLockService.swift */, + 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */, 490BEADEFB2D6B7C9F618AE8 /* AppLockTimer.swift */, ); path = AppLock; @@ -3401,6 +3427,7 @@ 16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.swift */, 7D0CBC76C80E04345E11F2DB /* Application.swift */, 349C633291427A0F29C28C54 /* AppLockScreenUITests.swift */, + 5F088B61525099A48909743B /* AppLockSettingsScreenUITests.swift */, 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */, C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, 1D8866FE1CCCF10305FCACBC /* CallScreenUITests.swift */, @@ -3460,6 +3487,7 @@ children = ( AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */, DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */, + B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */, 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */, ); path = AppLock; @@ -3957,6 +3985,18 @@ path = Layout; sourceTree = ""; }; + CE39C9B97963CC30AB0859E5 /* AppLockSettingsScreen */ = { + isa = PBXGroup; + children = ( + 267C0279BB8D907E2C40DDCA /* AppLockSettingsScreenCoordinator.swift */, + B86F43BF1C26CB31B4BFA610 /* AppLockSettingsScreenModels.swift */, + 892EF45CCC5D2BF0FD1F770C /* AppLockSettingsScreenViewModel.swift */, + 33284693F54382F46CFD2EDD /* AppLockSettingsScreenViewModelProtocol.swift */, + 01FACE97F4C9E9F522D401EB /* View */, + ); + path = AppLockSettingsScreen; + sourceTree = ""; + }; D4DB8163C10389C069458252 /* RoomMemberListScreen */ = { isa = PBXGroup; children = ( @@ -4059,7 +4099,7 @@ isa = PBXGroup; children = ( 669239C03835CD8B51E0FFDB /* AnalyticsPromptScreen */, - 77566988A0A4F94744C3818B /* AppLockScreen */, + 13263FFEA7749D822B51AA90 /* AppLock */, E74CD7681375AD2EAA34D66B /* Authentication */, 53FB148CD26AFB6A5B9E20B3 /* BugReportScreen */, 1185EECDD07495D65AC84AFC /* CallScreen */, @@ -4218,6 +4258,14 @@ path = InviteUsersScreen; sourceTree = ""; }; + F49710373735A3B8BB76DFF7 /* View */ = { + isa = PBXGroup; + children = ( + 56D6F88FE35A0979D2821E06 /* AppLockScreen.swift */, + ); + path = View; + sourceTree = ""; + }; F5A65D1D3B83593598DC278D /* EmojiPickerScreen */ = { isa = PBXGroup; children = ( @@ -4758,6 +4806,7 @@ 890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */, 8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */, 77693820498ABF3508814D49 /* AppLockServiceTests.swift in Sources */, + 73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */, 0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */, EA78A7512AFB1E5451744EB1 /* AppRouteURLParserTests.swift in Sources */, 3EC698F80DDEEFA273857841 /* ArrayTests.swift in Sources */, @@ -4897,12 +4946,18 @@ 4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */, 9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */, 6851B077B4C913CC12DB6E77 /* AppLockFlowCoordinator.swift in Sources */, - 64DD8AB9CA0405A43043BDF8 /* AppLockScreen.swift in Sources */, - 0EAEA507586717B055441970 /* AppLockScreenCoordinator.swift in Sources */, - 4A945B96B87D61F873F48933 /* AppLockScreenModels.swift in Sources */, - BE641CE5F9036B9AD7367DF1 /* AppLockScreenViewModel.swift in Sources */, - 33094DB91C3A4131E76B2C07 /* AppLockScreenViewModelProtocol.swift in Sources */, + 06F8EDF52E33A2D36BCC1161 /* AppLockScreen.swift in Sources */, + 9912F9EB2D6589141A2957B4 /* AppLockScreenCoordinator.swift in Sources */, + 2DD9D0FE7CB5CFC80D071451 /* AppLockScreenModels.swift in Sources */, + 97969EF0B9C412CD38E5CA93 /* AppLockScreenViewModel.swift in Sources */, + E79D79CDAFE8BEBCC3AECA54 /* AppLockScreenViewModelProtocol.swift in Sources */, 1D623953F970D11F6F38499C /* AppLockService.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 */, EF890DEF0479E66548F2BA23 /* AppLockTimer.swift in Sources */, 355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */, 12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */, @@ -5486,6 +5541,7 @@ 8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */, BF675964C9159F718589C36A /* AnalyticsSettingsScreenUITests.swift in Sources */, F05516474DB42369FD976CEF /* AppLockScreenUITests.swift in Sources */, + 61C345258DD392477E79A3B5 /* AppLockSettingsScreenUITests.swift in Sources */, 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */, ACF094CF3BF02DBFA6DFDE60 /* AuthenticationCoordinatorUITests.swift in Sources */, 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index cc0585e4e6..4cec3d4b8b 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -5,7 +5,16 @@ "untranslated" = "Untranslated"; "screen_app_lock_title" = "%@ is locked"; +"screen_app_lock_settings_change_pin" = "Change PIN code"; +"screen_app_lock_settings_remove_pin" = "Remove PIN"; +"screen_app_lock_settings_enable_touch_id_ios" = "Allow Touch ID"; +"screen_app_lock_settings_enable_face_id_ios" = "Allow Face ID"; +"screen_app_lock_settings_enable_optic_id_ios" = "Allow Optic ID"; +"screen_app_lock_settings_enable_biometric_unlock" = "Allow biometric unlock"; +"screen_app_lock_settings_remove_pin_alert_title" = "Remove PIN?"; +"screen_app_lock_settings_remove_pin_alert_message" = "Are you sure you want to remove PIN?"; "common_unlock" = "Unlock"; +"common_screen_lock" = "Screen lock"; // MARK: - Soft logout diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index f56ccfbf9c..869a3be3ef 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -425,6 +425,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: PlaceholderScreenCoordinator()) let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession, navigationSplitCoordinator: navigationSplitCoordinator, + appLockService: appLockFlowCoordinator.appLockService, bugReportService: ServiceLocator.shared.bugReportService, roomTimelineControllerFactory: RoomTimelineControllerFactory(), appSettings: appSettings, diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 1984980c0d..a71ef50002 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -25,6 +25,7 @@ enum UserSessionFlowCoordinatorAction { class UserSessionFlowCoordinator: FlowCoordinatorProtocol { private let userSession: UserSessionProtocol private let navigationSplitCoordinator: NavigationSplitCoordinator + private let appLockService: AppLockServiceProtocol private let bugReportService: BugReportServiceProtocol private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol private let appSettings: AppSettings @@ -48,6 +49,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { init(userSession: UserSessionProtocol, navigationSplitCoordinator: NavigationSplitCoordinator, + appLockService: AppLockServiceProtocol, bugReportService: BugReportServiceProtocol, roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol, appSettings: AppSettings, @@ -55,6 +57,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { stateMachine = UserSessionFlowCoordinatorStateMachine() self.userSession = userSession self.navigationSplitCoordinator = navigationSplitCoordinator + self.appLockService = appLockService self.bugReportService = bugReportService self.roomTimelineControllerFactory = roomTimelineControllerFactory self.appSettings = appSettings @@ -328,6 +331,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { let parameters = SettingsScreenCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator, userIndicatorController: userIndicatorController, userSession: userSession, + appLockService: appLockService, bugReportService: bugReportService, notificationSettings: userSession.clientProxy.notificationSettings, appSettings: appSettings) diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index d274c4113c..9112194893 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -10,8 +10,26 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum UntranslatedL10n { + /// Screen lock + public static var commonScreenLock: String { return UntranslatedL10n.tr("Untranslated", "common_screen_lock") } /// Unlock public static var commonUnlock: String { return UntranslatedL10n.tr("Untranslated", "common_unlock") } + /// Change PIN code + public static var screenAppLockSettingsChangePin: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_change_pin") } + /// Allow biometric unlock + public static var screenAppLockSettingsEnableBiometricUnlock: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_enable_biometric_unlock") } + /// Allow Face ID + public static var screenAppLockSettingsEnableFaceIdIos: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_enable_face_id_ios") } + /// Allow Optic ID + public static var screenAppLockSettingsEnableOpticIdIos: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_enable_optic_id_ios") } + /// Allow Touch ID + public static var screenAppLockSettingsEnableTouchIdIos: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_enable_touch_id_ios") } + /// Remove PIN + public static var screenAppLockSettingsRemovePin: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_remove_pin") } + /// Are you sure you want to remove PIN? + public static var screenAppLockSettingsRemovePinAlertMessage: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_remove_pin_alert_message") } + /// Remove PIN? + public static var screenAppLockSettingsRemovePinAlertTitle: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_remove_pin_alert_title") } /// %@ is locked public static func screenAppLockTitle(_ p1: Any) -> String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_title", String(describing: p1)) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 85e7e22aaa..e5e810c711 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2,11 +2,12 @@ // DO NOT EDIT // swiftlint:disable all +import AnalyticsEvents import Combine import Foundation -import SwiftUI -import AnalyticsEvents +import LocalAuthentication import MatrixRustSDK +import SwiftUI class AnalyticsClientMock: AnalyticsClientProtocol { var isRunning: Bool { get { return underlyingIsRunning } @@ -115,6 +116,128 @@ class AnalyticsClientMock: AnalyticsClientProtocol { updateUserPropertiesClosure?(userProperties) } } +class AppLockServiceMock: AppLockServiceProtocol { + var isEnabled: Bool { + get { return underlyingIsEnabled } + set(value) { underlyingIsEnabled = value } + } + var underlyingIsEnabled: Bool! + var biometryType: LABiometryType { + get { return underlyingBiometryType } + set(value) { underlyingBiometryType = value } + } + var underlyingBiometryType: LABiometryType! + var biometricUnlockEnabled: Bool { + get { return underlyingBiometricUnlockEnabled } + set(value) { underlyingBiometricUnlockEnabled = value } + } + var underlyingBiometricUnlockEnabled: Bool! + + //MARK: - setupPINCode + + var setupPINCodeCallsCount = 0 + var setupPINCodeCalled: Bool { + return setupPINCodeCallsCount > 0 + } + var setupPINCodeReceivedPinCode: String? + var setupPINCodeReceivedInvocations: [String] = [] + var setupPINCodeReturnValue: Result! + var setupPINCodeClosure: ((String) -> Result)? + + func setupPINCode(_ pinCode: String) -> Result { + setupPINCodeCallsCount += 1 + setupPINCodeReceivedPinCode = pinCode + setupPINCodeReceivedInvocations.append(pinCode) + if let setupPINCodeClosure = setupPINCodeClosure { + return setupPINCodeClosure(pinCode) + } else { + return setupPINCodeReturnValue + } + } + //MARK: - disable + + var disableCallsCount = 0 + var disableCalled: Bool { + return disableCallsCount > 0 + } + var disableClosure: (() -> Void)? + + func disable() { + disableCallsCount += 1 + disableClosure?() + } + //MARK: - applicationDidEnterBackground + + var applicationDidEnterBackgroundCallsCount = 0 + var applicationDidEnterBackgroundCalled: Bool { + return applicationDidEnterBackgroundCallsCount > 0 + } + var applicationDidEnterBackgroundClosure: (() -> Void)? + + func applicationDidEnterBackground() { + applicationDidEnterBackgroundCallsCount += 1 + applicationDidEnterBackgroundClosure?() + } + //MARK: - computeNeedsUnlock + + var computeNeedsUnlockWillEnterForegroundAtCallsCount = 0 + var computeNeedsUnlockWillEnterForegroundAtCalled: Bool { + return computeNeedsUnlockWillEnterForegroundAtCallsCount > 0 + } + var computeNeedsUnlockWillEnterForegroundAtReceivedDate: Date? + var computeNeedsUnlockWillEnterForegroundAtReceivedInvocations: [Date] = [] + var computeNeedsUnlockWillEnterForegroundAtReturnValue: Bool! + var computeNeedsUnlockWillEnterForegroundAtClosure: ((Date) -> Bool)? + + func computeNeedsUnlock(willEnterForegroundAt date: Date) -> Bool { + computeNeedsUnlockWillEnterForegroundAtCallsCount += 1 + computeNeedsUnlockWillEnterForegroundAtReceivedDate = date + computeNeedsUnlockWillEnterForegroundAtReceivedInvocations.append(date) + if let computeNeedsUnlockWillEnterForegroundAtClosure = computeNeedsUnlockWillEnterForegroundAtClosure { + return computeNeedsUnlockWillEnterForegroundAtClosure(date) + } else { + return computeNeedsUnlockWillEnterForegroundAtReturnValue + } + } + //MARK: - unlock + + var unlockWithCallsCount = 0 + var unlockWithCalled: Bool { + return unlockWithCallsCount > 0 + } + var unlockWithReceivedPinCode: String? + var unlockWithReceivedInvocations: [String] = [] + var unlockWithReturnValue: Bool! + var unlockWithClosure: ((String) -> Bool)? + + func unlock(with pinCode: String) -> Bool { + unlockWithCallsCount += 1 + unlockWithReceivedPinCode = pinCode + unlockWithReceivedInvocations.append(pinCode) + if let unlockWithClosure = unlockWithClosure { + return unlockWithClosure(pinCode) + } else { + return unlockWithReturnValue + } + } + //MARK: - unlockWithBiometrics + + var unlockWithBiometricsCallsCount = 0 + var unlockWithBiometricsCalled: Bool { + return unlockWithBiometricsCallsCount > 0 + } + var unlockWithBiometricsReturnValue: Bool! + var unlockWithBiometricsClosure: (() -> Bool)? + + func unlockWithBiometrics() -> Bool { + unlockWithBiometricsCallsCount += 1 + if let unlockWithBiometricsClosure = unlockWithBiometricsClosure { + return unlockWithBiometricsClosure() + } else { + return unlockWithBiometricsReturnValue + } + } +} class AudioConverterMock: AudioConverterProtocol { //MARK: - convertToOpusOgg diff --git a/ElementX/Sources/Screens/AppLockScreen/AppLockScreenCoordinator.swift b/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenCoordinator.swift similarity index 100% rename from ElementX/Sources/Screens/AppLockScreen/AppLockScreenCoordinator.swift rename to ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenCoordinator.swift diff --git a/ElementX/Sources/Screens/AppLockScreen/AppLockScreenModels.swift b/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenModels.swift similarity index 100% rename from ElementX/Sources/Screens/AppLockScreen/AppLockScreenModels.swift rename to ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenModels.swift diff --git a/ElementX/Sources/Screens/AppLockScreen/AppLockScreenViewModel.swift b/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenViewModel.swift similarity index 100% rename from ElementX/Sources/Screens/AppLockScreen/AppLockScreenViewModel.swift rename to ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenViewModel.swift diff --git a/ElementX/Sources/Screens/AppLockScreen/AppLockScreenViewModelProtocol.swift b/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenViewModelProtocol.swift similarity index 100% rename from ElementX/Sources/Screens/AppLockScreen/AppLockScreenViewModelProtocol.swift rename to ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenViewModelProtocol.swift diff --git a/ElementX/Sources/Screens/AppLockScreen/View/AppLockScreen.swift b/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift similarity index 90% rename from ElementX/Sources/Screens/AppLockScreen/View/AppLockScreen.swift rename to ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift index 9fef189274..e2e7de7fdf 100644 --- a/ElementX/Sources/Screens/AppLockScreen/View/AppLockScreen.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift @@ -47,8 +47,7 @@ struct AppLockScreen: View { // Add TestablePreview conformance once we have designs. struct AppLockScreen_Previews: PreviewProvider { - static let viewModel = AppLockScreenViewModel(appLockService: AppLockService(keychainController: KeychainControllerMock(), - appSettings: ServiceLocator.shared.settings)) + static let viewModel = AppLockScreenViewModel(appLockService: AppLockServiceMock.mock()) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/AppLockSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/AppLockSettingsScreenCoordinator.swift new file mode 100644 index 0000000000..254ce2b554 --- /dev/null +++ b/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/AppLockSettingsScreenCoordinator.swift @@ -0,0 +1,62 @@ +// +// 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 Combine +import SwiftUI + +struct AppLockSettingsScreenCoordinatorParameters { + let appLockService: AppLockServiceProtocol +} + +enum AppLockSettingsScreenCoordinatorAction { + case done +} + +final class AppLockSettingsScreenCoordinator: CoordinatorProtocol { + private let parameters: AppLockSettingsScreenCoordinatorParameters + private var viewModel: AppLockSettingsScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: AppLockSettingsScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = AppLockSettingsScreenViewModel(appLockService: parameters.appLockService) + } + + func start() { + viewModel.actions.sink { [weak self] action in + MXLog.info("Coordinator: received view model action: \(action)") + + guard let self else { return } + switch action { + case .changePINCode: + break + case .done: + self.actionsSubject.send(.done) + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(AppLockSettingsScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/AppLockSettingsScreenModels.swift b/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/AppLockSettingsScreenModels.swift new file mode 100644 index 0000000000..115b6fa946 --- /dev/null +++ b/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/AppLockSettingsScreenModels.swift @@ -0,0 +1,65 @@ +// +// 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 Foundation +import LocalAuthentication + +enum AppLockSettingsScreenViewModelAction { + /// The user would like to enter new PIN code. + case changePINCode + case done +} + +struct AppLockSettingsScreenViewState: BindableState { + let biometryType: LABiometryType + var bindings: AppLockSettingsScreenViewStateBindings + + var supportsBiometry: Bool { biometryType != .none } + var enableBiometryTitle: String { + switch biometryType { + case .none: + L10n.commonError + case .touchID: + UntranslatedL10n.screenAppLockSettingsEnableTouchIdIos + case .faceID: + UntranslatedL10n.screenAppLockSettingsEnableFaceIdIos + case .opticID: + UntranslatedL10n.screenAppLockSettingsEnableOpticIdIos + @unknown default: + UntranslatedL10n.screenAppLockSettingsEnableBiometricUnlock + } + } +} + +struct AppLockSettingsScreenViewStateBindings { + var enableBiometrics: Bool + var alertInfo: AlertInfo? +} + +enum AppLockSettingsScreenAlertType { + /// The alert shown to confirm the user would like to remove their PIN. + case confirmRemovePINCode +} + +enum AppLockSettingsScreenViewAction { + /// The user would like to enter a new PIN code. + case changePINCode + /// The user would like to disable the App Lock feature. + case disable + /// The user has toggled the biometrics setting. + case enableBiometricsChanged + case done +} diff --git a/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/AppLockSettingsScreenViewModel.swift b/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/AppLockSettingsScreenViewModel.swift new file mode 100644 index 0000000000..16b3be5326 --- /dev/null +++ b/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/AppLockSettingsScreenViewModel.swift @@ -0,0 +1,69 @@ +// +// 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 Combine +import SwiftUI + +typealias AppLockSettingsScreenViewModelType = StateStoreViewModel + +class AppLockSettingsScreenViewModel: AppLockSettingsScreenViewModelType, AppLockSettingsScreenViewModelProtocol { + private let appLockService: AppLockServiceProtocol + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(appLockService: AppLockServiceProtocol) { + self.appLockService = appLockService + super.init(initialViewState: AppLockSettingsScreenViewState(biometryType: appLockService.biometryType, + bindings: .init(enableBiometrics: appLockService.biometricUnlockEnabled))) + } + + // MARK: - Public + + override func process(viewAction: AppLockSettingsScreenViewAction) { + MXLog.info("View model: received view action: \(viewAction)") + + switch viewAction { + case .changePINCode: + actionsSubject.send(.changePINCode) + case .disable: + showRemovePINAlert() + case .enableBiometricsChanged: + appLockService.biometricUnlockEnabled = state.bindings.enableBiometrics + case .done: + actionsSubject.send(.done) + } + } + + // MARK: - Private + + /// Shows a confirmation alert to the user before removing their PIN code. + private func showRemovePINAlert() { + state.bindings.alertInfo = .init(id: .confirmRemovePINCode, + title: UntranslatedL10n.screenAppLockSettingsRemovePinAlertTitle, + message: UntranslatedL10n.screenAppLockSettingsRemovePinAlertMessage, + primaryButton: .init(title: L10n.actionYes) { self.completeRemovePIN() }, + secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) + } + + /// Removes the user's PIN code, disabling the App Lock feature. + private func completeRemovePIN() { + appLockService.disable() + actionsSubject.send(.done) + } +} diff --git a/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/AppLockSettingsScreenViewModelProtocol.swift b/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/AppLockSettingsScreenViewModelProtocol.swift new file mode 100644 index 0000000000..8fd9e1b6f9 --- /dev/null +++ b/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/AppLockSettingsScreenViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// 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 Combine + +@MainActor +protocol AppLockSettingsScreenViewModelProtocol { + var actions: AnyPublisher { get } + var context: AppLockSettingsScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/View/AppLockSettingsScreen.swift b/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/View/AppLockSettingsScreen.swift new file mode 100644 index 0000000000..9e0a9cae8d --- /dev/null +++ b/ElementX/Sources/Screens/AppLock/AppLockSettingsScreen/View/AppLockSettingsScreen.swift @@ -0,0 +1,72 @@ +// +// 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 Compound +import SwiftUI + +struct AppLockSettingsScreen: View { + @ObservedObject var context: AppLockSettingsScreenViewModel.Context + + var body: some View { + Form { + Section { + ListRow(label: .plain(title: UntranslatedL10n.screenAppLockSettingsChangePin), + kind: .button { context.send(viewAction: .changePINCode) }) + ListRow(label: .plain(title: UntranslatedL10n.screenAppLockSettingsRemovePin, role: .destructive), + kind: .button { context.send(viewAction: .disable) }) + } + + if context.viewState.supportsBiometry { + Section { + ListRow(label: .plain(title: context.viewState.enableBiometryTitle), + kind: .toggle($context.enableBiometrics)) + .onChange(of: context.enableBiometrics) { _ in + context.send(viewAction: .enableBiometricsChanged) + } + } + } + } + .compoundList() + .navigationTitle(L10n.commonSettings) + .navigationBarTitleDisplayMode(.inline) + .alert(item: $context.alertInfo) + } +} + +// MARK: - Previews + +struct AppLockSettingsScreen_Previews: PreviewProvider, TestablePreview { + static let faceIDViewModel = AppLockSettingsScreenViewModel(appLockService: AppLockServiceMock.mock(biometryType: .faceID)) + static let touchIDViewModel = AppLockSettingsScreenViewModel(appLockService: AppLockServiceMock.mock(biometryType: .touchID)) + static let biometricsUnavailableViewModel = AppLockSettingsScreenViewModel(appLockService: AppLockServiceMock.mock(biometryType: .none)) + + static var previews: some View { + NavigationStack { + AppLockSettingsScreen(context: faceIDViewModel.context) + } + .previewDisplayName("Face ID") + + NavigationStack { + AppLockSettingsScreen(context: touchIDViewModel.context) + } + .previewDisplayName("Touch ID") + + NavigationStack { + AppLockSettingsScreen(context: biometricsUnavailableViewModel.context) + } + .previewDisplayName("No Biometrics") + } +} diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift index ff114a08a3..e872f4b352 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift @@ -21,6 +21,7 @@ struct SettingsScreenCoordinatorParameters { weak var navigationStackCoordinator: NavigationStackCoordinator? weak var userIndicatorController: UserIndicatorControllerProtocol? let userSession: UserSessionProtocol + let appLockService: AppLockServiceProtocol let bugReportService: BugReportServiceProtocol let notificationSettings: NotificationSettingsProxyProtocol let appSettings: AppSettings @@ -48,7 +49,7 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { init(parameters: SettingsScreenCoordinatorParameters) { self.parameters = parameters - viewModel = SettingsScreenViewModel(userSession: parameters.userSession, appSettings: ServiceLocator.shared.settings) + viewModel = SettingsScreenViewModel(userSession: parameters.userSession, appSettings: parameters.appSettings) viewModel.actions .sink { [weak self] action in @@ -63,6 +64,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { presentAccountProfileURL() case .analytics: presentAnalyticsScreen() + case .appLock: + presentAppLockSettingsScreen() case .reportBug: presentBugReportScreen() case .about: @@ -133,11 +136,16 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { } private func presentAnalyticsScreen() { - let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(appSettings: ServiceLocator.shared.settings, + let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(appSettings: parameters.appSettings, analytics: ServiceLocator.shared.analytics)) parameters.navigationStackCoordinator?.push(coordinator) } + private func presentAppLockSettingsScreen() { + let coordinator = AppLockSettingsScreenCoordinator(parameters: .init(appLockService: parameters.appLockService)) + parameters.navigationStackCoordinator?.push(coordinator) + } + private func presentBugReportScreen() { let params = BugReportScreenCoordinatorParameters(bugReportService: parameters.bugReportService, userID: parameters.userSession.userID, diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift index d53a12ca09..e938090e1b 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift @@ -22,6 +22,7 @@ enum SettingsScreenViewModelAction { case userDetails case accountProfile case analytics + case appLock case reportBug case about case sessionVerification @@ -40,6 +41,7 @@ struct SettingsScreenViewState: BindableState { var userAvatarURL: URL? var userDisplayName: String? var showSessionVerificationSection: Bool + var showAppLockSettings: Bool var showDeveloperOptions: Bool /// The presentation anchor used to display the OIDC account URL. @@ -51,6 +53,7 @@ enum SettingsScreenViewAction { case userDetails case accountProfile case analytics + case appLock case reportBug case about case sessionVerification diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift index b52f00c573..893c8fcad3 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift @@ -43,6 +43,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo accountProfileURL: userSession.clientProxy.accountURL(action: .profile), accountSessionsListURL: userSession.clientProxy.accountURL(action: .sessionsList), showSessionVerificationSection: showSessionVerificationSection, + showAppLockSettings: appSettings.appLockFlowEnabled, showDeveloperOptions: appSettings.canShowDeveloperOptions), imageProvider: userSession.mediaProvider) @@ -55,6 +56,10 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo .receive(on: DispatchQueue.main) .weakAssign(to: \.state.userDisplayName, on: self) .store(in: &cancellables) + + appSettings.$appLockFlowEnabled + .weakAssign(to: \.state.showAppLockSettings, on: self) + .store(in: &cancellables) Task { await userSession.clientProxy.loadUserAvatarURL() @@ -86,6 +91,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo actionsSubject.send(.accountProfile) case .analytics: actionsSubject.send(.analytics) + case .appLock: + actionsSubject.send(.appLock) case .reportBug: actionsSubject.send(.reportBug) case .about: diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift index a4836da772..ed82cfacc8 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift @@ -118,6 +118,15 @@ struct SettingsScreen: View { }) .accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics) + if context.viewState.showAppLockSettings { + ListRow(label: .default(title: UntranslatedL10n.commonScreenLock, + systemIcon: .lock), + kind: .navigationLink { + context.send(viewAction: .appLock) + }) + .accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics) + } + ListRow(label: .default(title: L10n.commonReportABug, systemIcon: .ladybug), kind: .navigationLink { diff --git a/ElementX/Sources/Services/AppLock/AppLockService.swift b/ElementX/Sources/Services/AppLock/AppLockService.swift index adbad8b20d..29250f7f5e 100644 --- a/ElementX/Sources/Services/AppLock/AppLockService.swift +++ b/ElementX/Sources/Services/AppLock/AppLockService.swift @@ -16,40 +16,6 @@ import LocalAuthentication -enum AppLockServiceError: Error { - /// The operation failed to access the keychain. - case keychainError - /// The PIN code was rejected because it isn't long enough, or contains invalid characters. - case invalidPIN - /// The PIN code was rejected as an insecure choice. - case weakPIN -} - -@MainActor -protocol AppLockServiceProtocol { - /// The app has been configured to automatically lock with a PIN code. - var isEnabled: Bool { get } - /// The type of biometric authentication supported by the device. - var biometryType: LABiometryType { get } - /// Whether or not the user has enabled unlock via TouchID, FaceID or (possibly) OpticID. - var biometricUnlockEnabled: Bool { get set } - - /// Sets the user's PIN code used to unlock the app. - func setupPINCode(_ pinCode: String) -> Result - /// Disables the App Lock feature, removing the user's stored PIN code. - func disable() - - /// Informs the service that the app has entered the background. - func applicationDidEnterBackground() - /// Decides whether the app should be unlocked with a PIN code/biometrics on foregrounding. - func computeNeedsUnlock(willEnterForegroundAt date: Date) -> Bool - - /// Attempt to unlock the app with the supplied PIN code. - func unlock(with pinCode: String) -> Bool - /// Attempt to unlock the app using FaceID or TouchID. - func unlockWithBiometrics() -> Bool -} - /// The service responsible for locking and unlocking the app. class AppLockService: AppLockServiceProtocol { private let keychainController: KeychainControllerProtocol @@ -76,6 +42,8 @@ class AppLockService: AppLockServiceProtocol { self.keychainController = keychainController self.appSettings = appSettings timer = AppLockTimer(gracePeriod: appSettings.appLockGracePeriod) + + configureBiometrics() } func setupPINCode(_ pinCode: String) -> Result { @@ -116,6 +84,16 @@ class AppLockService: AppLockServiceProtocol { // MARK: - Private + /// Queries the context for supported biometrics. + private func configureBiometrics() { + var error: NSError? + context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) + + if let error { + MXLog.error("Biometrics error: \(error)") + } + } + /// Ensures that a provided PIN code is long enough and only contains digits. private func validate(_ pinCode: String) -> Bool { pinCode.count == 4 && pinCode.allSatisfy(\.isNumber) diff --git a/ElementX/Sources/Services/AppLock/AppLockServiceProtocol.swift b/ElementX/Sources/Services/AppLock/AppLockServiceProtocol.swift new file mode 100644 index 0000000000..2c20d22bc4 --- /dev/null +++ b/ElementX/Sources/Services/AppLock/AppLockServiceProtocol.swift @@ -0,0 +1,65 @@ +// +// 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 LocalAuthentication + +enum AppLockServiceError: Error { + /// The operation failed to access the keychain. + case keychainError + /// The PIN code was rejected because it isn't long enough, or contains invalid characters. + case invalidPIN + /// The PIN code was rejected as an insecure choice. + case weakPIN +} + +@MainActor +protocol AppLockServiceProtocol: AnyObject { + /// The app has been configured to automatically lock with a PIN code. + var isEnabled: Bool { get } + /// The type of biometric authentication supported by the device. + var biometryType: LABiometryType { get } + /// Whether or not the user has enabled unlock via TouchID, FaceID or (possibly) OpticID. + var biometricUnlockEnabled: Bool { get set } + + /// Sets the user's PIN code used to unlock the app. + func setupPINCode(_ pinCode: String) -> Result + /// Disables the App Lock feature, removing the user's stored PIN code. + func disable() + + /// Informs the service that the app has entered the background. + func applicationDidEnterBackground() + /// Decides whether the app should be unlocked with a PIN code/biometrics on foregrounding. + func computeNeedsUnlock(willEnterForegroundAt date: Date) -> Bool + + /// Attempt to unlock the app with the supplied PIN code. + func unlock(with pinCode: String) -> Bool + /// Attempt to unlock the app using FaceID or TouchID. + func unlockWithBiometrics() -> Bool +} + +// sourcery: AutoMockable +extension AppLockServiceProtocol { } + +extension AppLockServiceMock { + static func mock(pinCode: String? = "2023", biometryType: LABiometryType = .faceID) -> AppLockServiceMock { + let mock = AppLockServiceMock() + mock.isEnabled = pinCode != nil + mock.underlyingBiometryType = biometryType + mock.underlyingBiometricUnlockEnabled = biometryType != .none + mock.unlockWithClosure = { $0 == pinCode } + return mock + } +} diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 54b7d55c57..2ffa743414 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -161,6 +161,12 @@ class MockScreen: Identifiable { let appLockService = AppLockService(keychainController: KeychainControllerMock(), appSettings: ServiceLocator.shared.settings) let coordinator = AppLockScreenCoordinator(parameters: .init(appLockService: appLockService)) return coordinator + case .appLockSettingsScreen: + let navigationStackCoordinator = NavigationStackCoordinator() + let appLockService = AppLockServiceMock.mock(biometryType: .faceID) + let coordinator = AppLockSettingsScreenCoordinator(parameters: .init(appLockService: appLockService)) + navigationStackCoordinator.setRootCoordinator(coordinator) + return navigationStackCoordinator case .home: let navigationStackCoordinator = NavigationStackCoordinator() let session = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:matrix.org"), @@ -178,7 +184,11 @@ class MockScreen: Identifiable { let clientProxy = MockClientProxy(userID: "@mock:client.com") let coordinator = SettingsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, userIndicatorController: nil, - userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock()), + userSession: MockUserSession(clientProxy: clientProxy, + mediaProvider: MockMediaProvider(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock()), + appLockService: AppLockService(keychainController: KeychainControllerMock(), + appSettings: ServiceLocator.shared.settings), bugReportService: BugReportServiceMock(), notificationSettings: NotificationSettingsProxyMock(with: .init()), appSettings: ServiceLocator.shared.settings)) @@ -444,6 +454,8 @@ class MockScreen: Identifiable { let coordinator = UserSessionFlowCoordinator(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock()), navigationSplitCoordinator: navigationSplitCoordinator, + appLockService: AppLockService(keychainController: KeychainControllerMock(), + appSettings: ServiceLocator.shared.settings), bugReportService: BugReportServiceMock(), roomTimelineControllerFactory: MockRoomTimelineControllerFactory(), appSettings: appSettings, diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index 00780d9862..28a90ce4e0 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -30,6 +30,7 @@ enum UITestsScreenIdentifier: String { case migration case templateScreen case appLockScreen + case appLockSettingsScreen case home case settings case bugReport diff --git a/Tools/Sourcery/sourcery_automockable_config.yml b/Tools/Sourcery/sourcery_automockable_config.yml index ec5ca402c6..71686e5117 100644 --- a/Tools/Sourcery/sourcery_automockable_config.yml +++ b/Tools/Sourcery/sourcery_automockable_config.yml @@ -9,4 +9,4 @@ output: ../../ElementX/Sources/Mocks/Generated/GeneratedMocks.swift args: automMockableTestableImports: [] - autoMockableImports: [Combine, Foundation, SwiftUI, AnalyticsEvents, MatrixRustSDK] + autoMockableImports: [AnalyticsEvents, Combine, Foundation, LocalAuthentication, MatrixRustSDK, SwiftUI] diff --git a/UITests/Sources/AppLockSettingsScreenUITests.swift b/UITests/Sources/AppLockSettingsScreenUITests.swift new file mode 100644 index 0000000000..73ae05b11c --- /dev/null +++ b/UITests/Sources/AppLockSettingsScreenUITests.swift @@ -0,0 +1,26 @@ +// +// 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) + } +} diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.appLockSettingsScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.appLockSettingsScreen.png new file mode 100644 index 0000000000..8901872235 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.appLockSettingsScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a64be16e5c8b285e9296e8b8bc5b96424ce729ad6340177e3329a94d09950c3 +size 72850 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.appLockSettingsScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.appLockSettingsScreen.png new file mode 100644 index 0000000000..1aa31b8418 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.appLockSettingsScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98983e4aafbf08057641ff375b91ea5dfade32bdcf53eb6870a376cb12e8b02f +size 81356 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.appLockSettingsScreen.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.appLockSettingsScreen.png new file mode 100644 index 0000000000..5548783a3e --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.appLockSettingsScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:238727d51ac4349122c540faa857a99cfd7573d9ab6448a8bfacabf9bf622660 +size 75120 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.appLockSettingsScreen.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.appLockSettingsScreen.png new file mode 100644 index 0000000000..a234fce982 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.appLockSettingsScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4af3c867446df47af8569f58105b92076797f6b33c18282e0962655e318beaa +size 85562 diff --git a/UnitTests/Sources/AppLock/AppLockScreenViewModelTests.swift b/UnitTests/Sources/AppLock/AppLockScreenViewModelTests.swift index 8016c7d73a..4c77886585 100644 --- a/UnitTests/Sources/AppLock/AppLockScreenViewModelTests.swift +++ b/UnitTests/Sources/AppLock/AppLockScreenViewModelTests.swift @@ -33,6 +33,10 @@ class AppLockScreenViewModelTests: XCTestCase { viewModel = AppLockScreenViewModel(appLockService: appLockService) } + override func tearDown() { + AppSettings.reset() + } + func testUnlock() async throws { // Given a valid PIN code. let pinCode = "2023" diff --git a/UnitTests/Sources/AppLock/AppLockSettingsScreenViewModelTests.swift b/UnitTests/Sources/AppLock/AppLockSettingsScreenViewModelTests.swift new file mode 100644 index 0000000000..460b77af50 --- /dev/null +++ b/UnitTests/Sources/AppLock/AppLockSettingsScreenViewModelTests.swift @@ -0,0 +1,62 @@ +// +// 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 XCTest + +@testable import ElementX + +@MainActor +class AppLockSettingsScreenViewModelTests: XCTestCase { + var appLockService: AppLockServiceProtocol! + var keychainController: KeychainControllerMock! + var viewModel: AppLockSettingsScreenViewModelProtocol! + + var context: AppLockSettingsScreenViewModelType.Context { + viewModel.context + } + + override func setUpWithError() throws { + AppSettings.reset() + let appSettings = AppSettings() + appSettings.appLockFlowEnabled = true + + keychainController = KeychainControllerMock() + appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings) + + viewModel = AppLockSettingsScreenViewModel(appLockService: AppLockServiceMock.mock()) + } + + override func tearDown() { + AppSettings.reset() + } + + func testDisablingShowsAlert() { + // Given a fresh screen with the PIN code enabled. + let pinCode = "2023" + keychainController.pinCodeReturnValue = pinCode + keychainController.containsPINCodeReturnValue = true + + XCTAssertNil(context.alertInfo) + XCTAssertTrue(appLockService.isEnabled) + + // When disabling the PIN code lock. + context.send(viewAction: .disable) + + // Then an alert should be shown before disabling it. + XCTAssertNotNil(context.alertInfo) + XCTAssertTrue(appLockService.isEnabled) + } +} diff --git a/UnitTests/__Snapshots__/PreviewTests/test_appLockSettingsScreen.Face-ID.png b/UnitTests/__Snapshots__/PreviewTests/test_appLockSettingsScreen.Face-ID.png new file mode 100644 index 0000000000..0ec264cb69 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_appLockSettingsScreen.Face-ID.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c682d5a5dd971f1779f2a36f3e12ef372dbb687604524da0f07b69f46a0827d +size 94294 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_appLockSettingsScreen.No-Biometrics.png b/UnitTests/__Snapshots__/PreviewTests/test_appLockSettingsScreen.No-Biometrics.png new file mode 100644 index 0000000000..971325f39b --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_appLockSettingsScreen.No-Biometrics.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52d164bac6f06f0641d16e27b08672a656fc35f483cc5f75b049140f58020e85 +size 83047 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_appLockSettingsScreen.Touch-ID.png b/UnitTests/__Snapshots__/PreviewTests/test_appLockSettingsScreen.Touch-ID.png new file mode 100644 index 0000000000..75d39315d2 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_appLockSettingsScreen.Touch-ID.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9692e6f776b9d7ac92ea0becbbfc2463fac901d715ff197ae5555c4dcc2a4a7a +size 93863 diff --git a/changelog.d/pr-1917.wip b/changelog.d/pr-1917.wip new file mode 100644 index 0000000000..3937a89c54 --- /dev/null +++ b/changelog.d/pr-1917.wip @@ -0,0 +1 @@ +Add the App Lock settings screen. \ No newline at end of file