diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index fc28ef73b8..0a43bfd843 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */; }; 071A017E415AD378F2961B11 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */; }; + 08CB4BD12CEEDE6AAE4A18DD /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035177BCD8E8308B098AC3C2 /* WindowManager.swift */; }; 095C0ACFC234E0550A6404C5 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */; }; 095D3906CF2F940C2D2D17CC /* RoomFlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */; }; 09713669577CDA8D012EE380 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 6647C55D93508C7CE9D954A5 /* MatrixRustSDK */; }; @@ -52,6 +53,7 @@ 0E08BB72B2258652CF501A8B /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 78B28D75FF7AF8E6146DEE2A /* LRUCache */; }; 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 */; }; 0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */; }; @@ -94,6 +96,7 @@ 1C8BC70A18060677E295A846 /* ShareToMapsAppActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */; }; 1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */; }; 1D5DC685CED904386C89B7DA /* NSRegularExpresion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */; }; + 1D623953F970D11F6F38499C /* AppLockService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851B95BB98649B8E773D6790 /* AppLockService.swift */; }; 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */; }; 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; }; 1F04C63D4FA95948E3F52147 /* FileRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */; }; @@ -157,6 +160,7 @@ 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 */; }; 33CAC1226DFB8B5D8447D286 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; }; 340D39DB87F3800D53A6A621 /* EmojiPickerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */; }; @@ -236,6 +240,7 @@ 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 */; }; @@ -317,6 +322,7 @@ 64AB99285DC4437C0DDE9585 /* MenuSheetLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49ABAB186CF00B15C5521D04 /* MenuSheetLabelStyle.swift */; }; 64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */; }; 64D05250CEDE8B604119F6E6 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981663D961C94270FA035FD0 /* Alert.swift */; }; + 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 */; }; @@ -333,6 +339,7 @@ 67D6E0700A9C1E676F6231F8 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = AD544C0FA48DFFB080920061 /* Collections */; }; 68184EF36396424FE19A727D /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; }; 6832733838C57A7D3FE8FEB5 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; }; + 6851B077B4C913CC12DB6E77 /* AppLockFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */; }; 6860721DB3091BE08164C132 /* MapAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B48B7AD4908C5C374517B892 /* MapAssets.xcassets */; }; 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; 695825D20A761C678809345D /* MessageForwardingScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52135BD9E0E7A091688F627A /* MessageForwardingScreenModels.swift */; }; @@ -568,6 +575,7 @@ A9A5801D5EE3D4D91F6DDADB /* AnalyticsSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */; }; A9D349478F7D4A2B1E40CEF9 /* LegalInformationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8977176AB534AA41630395BC /* LegalInformationScreenViewModelProtocol.swift */; }; AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */; }; + AA64AAE1C4BB96C7F2761CAB /* AppLockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F4BEE95A091150EEBF1C358 /* AppLockScreenViewModelTests.swift */; }; AA93B3F9B5DD097DEF79F981 /* NotificationSettingsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */; }; AAF0BBED840DF4A53EE85E77 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C2C69B8BA5A9702E7A8BC08F /* MatrixRustSDK */; }; ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; }; @@ -641,6 +649,7 @@ 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 */; }; @@ -809,6 +818,7 @@ EF0D0155DD104C7A41A2EB0E /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; }; EF5009AC03212227131C8AF2 /* RoomNotificationSettingsProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */; }; EF7924005216B8189898F370 /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */; }; + F05516474DB42369FD976CEF /* AppLockScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349C633291427A0F29C28C54 /* AppLockScreenUITests.swift */; }; F06CE9132855E81EBB6DDC32 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 940C605265DD82DA0C655E23 /* Kingfisher */; }; F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */; }; F0A26CD502C3A5868353B0FA /* ServerConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24DEE0682C95F897B6C7CB0D /* ServerConfirmationScreenViewModel.swift */; }; @@ -849,12 +859,14 @@ FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; }; FB0A9D06FC9122E37992D962 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; }; FB53CD9B74A15B3B94F9F788 /* CreateRoomModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B849D2FF2CC12BA411A1651 /* CreateRoomModels.swift */; }; + FB595EC9C00AB32F39034055 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A37E2FACFD041CE466223CD /* SceneDelegate.swift */; }; FB9A1DD83EF641A75ABBCE69 /* WaitlistScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C796FC1DFDBCDD5573D0360F /* WaitlistScreenViewModelTests.swift */; }; FBCCF1EA25A071324FCD8544 /* TimelineItemDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7023EB4F3B7C7D1FBA68638B /* TimelineItemDebugView.swift */; }; FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */; }; FC10228E73323BDC09526F97 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; }; FCD3F2B82CAB29A07887A127 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2B43F2AF7456567FE37270A7 /* KeychainAccess */; }; FCDA202B246F75BA28E10C5F /* MapTilerAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = E062C1750EFC8627DE4CAB8E /* MapTilerAuthorization.swift */; }; + FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */; }; FD4C21F8DA1E273DE94FCD1A /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */; }; FD4DEC88210F35C35B2FB386 /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A1398EFF65090FDA1CB639 /* ProcessInfo.swift */; }; FD762761C5D0C30E6255C3D8 /* ServerConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */; }; @@ -918,6 +930,7 @@ 02D155E09BF961BBA8F85263 /* InviteUsersScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenViewModel.swift; sourceTree = ""; }; 03277E40D0E0DE0712021A71 /* ServerConfirmationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenCoordinator.swift; sourceTree = ""; }; 033DB41C51865A2E83174E87 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; + 035177BCD8E8308B098AC3C2 /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; 0376C429FAB1687C3D905F3E /* MockCoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCoder.swift; sourceTree = ""; }; 03DD998E523D4EC93C7ED703 /* RoomNotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; 03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelTests.swift; sourceTree = ""; }; @@ -1070,6 +1083,7 @@ 33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; 340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModelProtocol.swift; sourceTree = ""; }; 342BEBC3C5FC3F9943C41C4C /* TemplateScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelProtocol.swift; sourceTree = ""; }; + 349C633291427A0F29C28C54 /* AppLockScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenUITests.swift; sourceTree = ""; }; 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReactionsView.swift; sourceTree = ""; }; 3558A15CFB934F9229301527 /* RestorationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationToken.swift; sourceTree = ""; }; 35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; @@ -1084,6 +1098,7 @@ 3948D16F021DFDB2CD26EAA8 /* MockBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBackgroundTaskService.swift; sourceTree = ""; }; 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadableFrameModifier.swift; sourceTree = ""; }; 39B6C8690AEA1E49FF1BAF95 /* MediaUploadPreviewScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenUITests.swift; sourceTree = ""; }; + 39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerMock.swift; sourceTree = ""; }; 3A328F9E556F5CFA89332017 /* CreatePollScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenViewModel.swift; sourceTree = ""; }; 3B5E97E9615A158C76B2AB77 /* DateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTests.swift; sourceTree = ""; }; 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenUITests.swift; sourceTree = ""; }; @@ -1154,6 +1169,7 @@ 4E47F18A9A077E351CEA10D4 /* TextBasedRoomTimelineViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewProtocol.swift; sourceTree = ""; }; 4E625B0EB2F86B37C14EF7E6 /* SettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenViewModel.swift; sourceTree = ""; }; 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxyProtocol.swift; sourceTree = ""; }; + 4F4BEE95A091150EEBF1C358 /* AppLockScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelTests.swift; sourceTree = ""; }; 4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinatorTests.swift; sourceTree = ""; }; 4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = ""; }; 4FDD775CFD72DD2D3C8A8390 /* NotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyProtocol.swift; sourceTree = ""; }; @@ -1190,6 +1206,7 @@ 58D295F0081084F38DB20893 /* RoomNotificationSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModelTests.swift; sourceTree = ""; }; 592A35163B0749C66BFD6186 /* MapLibreStaticMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreStaticMapView.swift; sourceTree = ""; }; 59846FA04E1DBBFDD8829C2A /* MessageForwardingScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenUITests.swift; sourceTree = ""; }; + 5A37E2FACFD041CE466223CD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = ""; }; 5B0D7955FFB19B584594844B /* OnboardingLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingLogo.swift; sourceTree = ""; }; 5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = ""; }; @@ -1306,6 +1323,7 @@ 84A87D0471D438A233C2CF4A /* RoomMemberDetailsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenViewModel.swift; sourceTree = ""; }; 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringTaskRunnerTests.swift; sourceTree = ""; }; 85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenViewModelProtocol.swift; sourceTree = ""; }; + 851B95BB98649B8E773D6790 /* AppLockService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockService.swift; sourceTree = ""; }; 851EF6258DF8B7EF129DC3AC /* WelcomeScreenScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenViewModelTests.swift; sourceTree = ""; }; 8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelTests.swift; sourceTree = ""; }; 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = ""; }; @@ -1318,6 +1336,7 @@ 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 = ""; }; 893777A4997BBDB68079D4F5 /* ArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayTests.swift; sourceTree = ""; }; @@ -1475,7 +1494,9 @@ BB23BEAF8831DC6A57E39F52 /* CreatePollScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenCoordinator.swift; sourceTree = ""; }; BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.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 = ""; }; @@ -1516,6 +1537,7 @@ C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomList.swift; sourceTree = ""; }; C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.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 = ""; }; @@ -1609,6 +1631,7 @@ 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 = ""; }; @@ -1679,6 +1702,7 @@ FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreen.swift; sourceTree = ""; }; FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = ""; }; FC2D505742FDA21FCDC4C18A /* AudioRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineView.swift; sourceTree = ""; }; + FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockFlowCoordinator.swift; sourceTree = ""; }; FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorController.swift; sourceTree = ""; }; FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerTests.swift; sourceTree = ""; }; FE87C931165F5E201CACBB87 /* MediaPlayerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProtocol.swift; sourceTree = ""; }; @@ -1858,6 +1882,7 @@ isa = PBXGroup; children = ( 4BF8D11D9ED15CFC373D0119 /* Analytics */, + 7803E03F759061C948D66B7E /* AppLock */, 984A887BA0294FE3B00CE9B1 /* AudioPlayer */, AAFDD509929A0CCF8BCE51EB /* Authentication */, EBBEB5471737E9D116DF4738 /* Background */, @@ -2427,6 +2452,14 @@ path = View; sourceTree = ""; }; + 4AEBBDDC75A39318FFF01EBF /* View */ = { + isa = PBXGroup; + children = ( + 891D46CF94626F05614829A2 /* AppLockScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 4B5DC42A1DB20ECEB0FF67CB /* Tests */ = { isa = PBXGroup; children = ( @@ -2552,6 +2585,7 @@ 593C7129C5927E25AD8B688F /* FlowCoordinators */ = { isa = PBXGroup; children = ( + FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */, 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */, C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */, E8774CF614849664B5B3C2A1 /* UserSessionFlowCoordinatorStateMachine.swift */, @@ -2707,6 +2741,15 @@ path = Manager; sourceTree = ""; }; + 703929219780FFABAC6380AA /* Windowing */ = { + isa = PBXGroup; + children = ( + 5A37E2FACFD041CE466223CD /* SceneDelegate.swift */, + 035177BCD8E8308B098AC3C2 /* WindowManager.swift */, + ); + path = Windowing; + sourceTree = ""; + }; 70B74A432C241E56A7ACE610 /* Settings */ = { isa = PBXGroup; children = ( @@ -2758,6 +2801,7 @@ children = ( 58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */, C687844F60BFF532D49A994C /* AnalyticsTests.swift */, + 4F4BEE95A091150EEBF1C358 /* AppLockScreenViewModelTests.swift */, E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */, 893777A4997BBDB68079D4F5 /* ArrayTests.swift */, AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */, @@ -2878,6 +2922,26 @@ 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 */, + ); + path = AppLock; + sourceTree = ""; + }; 780F74C73E826685A9DB289B /* Navigation */ = { isa = PBXGroup; children = ( @@ -3212,6 +3276,7 @@ AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */, 16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.swift */, 7D0CBC76C80E04345E11F2DB /* Application.swift */, + 349C633291427A0F29C28C54 /* AppLockScreenUITests.swift */, 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */, C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, A5DDF245FA51CF75F89E58A4 /* CreatePollScreenUITests.swift */, @@ -3423,6 +3488,7 @@ 7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */, 57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */, 780F74C73E826685A9DB289B /* Navigation */, + 703929219780FFABAC6380AA /* Windowing */, ); path = Application; sourceTree = ""; @@ -3723,6 +3789,7 @@ isa = PBXGroup; children = ( E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */, + 39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */, E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */, ); path = Keychain; @@ -3867,6 +3934,7 @@ isa = PBXGroup; children = ( 669239C03835CD8B51E0FFDB /* AnalyticsPromptScreen */, + 77566988A0A4F94744C3818B /* AppLockScreen */, E74CD7681375AD2EAA34D66B /* Authentication */, 53FB148CD26AFB6A5B9E20B3 /* BugReportScreen */, 27F2500AC8736AAE774520C0 /* ComposerToolbar */, @@ -4549,6 +4617,7 @@ files = ( A9A5801D5EE3D4D91F6DDADB /* AnalyticsSettingsScreenViewModelTests.swift in Sources */, 890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */, + AA64AAE1C4BB96C7F2761CAB /* AppLockScreenViewModelTests.swift in Sources */, EA78A7512AFB1E5451744EB1 /* AppRouteURLParserTests.swift in Sources */, 3EC698F80DDEEFA273857841 /* ArrayTests.swift in Sources */, 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */, @@ -4682,6 +4751,13 @@ A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */, 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 */, + 1D623953F970D11F6F38499C /* AppLockService.swift in Sources */, 355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */, 12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */, 9462C62798F47E39DCC182D2 /* Application.swift in Sources */, @@ -4840,6 +4916,7 @@ BD6D98676111DA8FC2BE4908 /* InvitesScreenViewModelProtocol.swift in Sources */, E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */, 1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */, + FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */, CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */, D5681C80D8281560AACE0035 /* Label.swift in Sources */, EEAE954289DE813A61656AE0 /* LayoutDirection.swift in Sources */, @@ -5065,6 +5142,7 @@ D8385A51A3D0FA9283556281 /* RoundedLabelItem.swift in Sources */, 50C90117FE25390BFBD40173 /* RustTracing.swift in Sources */, D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */, + FB595EC9C00AB32F39034055 /* SceneDelegate.swift in Sources */, 0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */, 0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */, 67160204A8D362BB7D4AD259 /* Search.swift in Sources */, @@ -5227,6 +5305,7 @@ BD2BF1EC73FFB0C01552ECDA /* WelcomeScreenScreenModels.swift in Sources */, DC1BB5EE5F4D9B6A1F98A77A /* WelcomeScreenScreenViewModel.swift in Sources */, 94CEF587A3994A36A46D8334 /* WelcomeScreenScreenViewModelProtocol.swift in Sources */, + 08CB4BD12CEEDE6AAE4A18DD /* WindowManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5237,6 +5316,7 @@ 795A854F63301DC6B46217B9 /* AccessibilityIdentifiers.swift in Sources */, 8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */, BF675964C9159F718589C36A /* AnalyticsSettingsScreenUITests.swift in Sources */, + F05516474DB42369FD976CEF /* AppLockScreenUITests.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 468acd206a..8ce95ae7af 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -4,6 +4,9 @@ /* Used for testing */ "untranslated" = "Untranslated"; +"screen_app_lock_title" = "%@ is locked"; +"common_unlock" = "Unlock"; + // MARK: - Soft logout "soft_logout_signin_title" = "Sign in"; diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index a8e24b7330..f56ccfbf9c 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -20,7 +20,7 @@ import MatrixRustSDK import SwiftUI import Version -class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, NotificationManagerDelegate { +class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, NotificationManagerDelegate, WindowManagerDelegate { private let stateMachine: AppCoordinatorStateMachine private let navigationRootCoordinator: NavigationRootCoordinator private let userSessionStore: UserSessionStoreProtocol @@ -43,8 +43,9 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, } } - private var userSessionFlowCoordinator: UserSessionFlowCoordinator? private var authenticationCoordinator: AuthenticationCoordinator? + private let appLockFlowCoordinator: AppLockFlowCoordinator + private var userSessionFlowCoordinator: UserSessionFlowCoordinator? private var softLogoutCoordinator: SoftLogoutScreenCoordinator? private let backgroundTaskService: BackgroundTaskServiceProtocol @@ -54,6 +55,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, private var clientProxyObserver: AnyCancellable? private var cancellables = Set() + let windowManager = WindowManager() let notificationManager: NotificationManagerProtocol private let appRouteURLParser: AppRouteURLParser @@ -94,10 +96,20 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, UIApplication.shared } - userSessionStore = UserSessionStore(backgroundTaskService: backgroundTaskService) - + let keychainController = KeychainController(service: .sessions, + accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) + userSessionStore = UserSessionStore(keychainController: keychainController, + backgroundTaskService: backgroundTaskService) + + let appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings) + appLockFlowCoordinator = AppLockFlowCoordinator(appLockService: appLockService, + navigationCoordinator: NavigationRootCoordinator()) + notificationManager = NotificationManager(notificationCenter: UNUserNotificationCenter.current(), appSettings: appSettings) + + windowManager.delegate = self + notificationManager.delegate = self notificationManager.start() @@ -117,6 +129,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, observeApplicationState() observeNetworkState() + observeAppLockChanges() registerBackgroundAppRefresh() } @@ -178,6 +191,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, stateMachine.processEvent(.createdUserSession) } + // MARK: - WindowManagerDelegate + + func windowManagerDidConfigureWindows(_ windowManager: WindowManager) { + windowManager.alternateWindow.rootViewController = UIHostingController(rootView: appLockFlowCoordinator.toPresentable()) + } + // MARK: - NotificationManagerDelegate func registerForRemoteNotifications() { @@ -553,6 +572,19 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, .store(in: &cancellables) } + private func observeAppLockChanges() { + appLockFlowCoordinator.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .lockApp: + windowManager.switchToAlternate() + case .unlockApp: + windowManager.switchToMain() + } + } + .store(in: &cancellables) + } + private func handleAppRoute(_ appRoute: AppRoute) { if let userSessionFlowCoordinator { userSessionFlowCoordinator.handleAppRoute(appRoute, animated: UIApplication.shared.applicationState == .active) diff --git a/ElementX/Sources/Application/AppDelegate.swift b/ElementX/Sources/Application/AppDelegate.swift index 8be8ee0a40..8fda08b06c 100644 --- a/ElementX/Sources/Application/AppDelegate.swift +++ b/ElementX/Sources/Application/AppDelegate.swift @@ -25,6 +25,13 @@ enum AppDelegateCallback { class AppDelegate: NSObject, UIApplicationDelegate { let callbacks = PassthroughSubject() + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Add a SceneDelegate to the SwiftUI scene so that we can connect up the WindowManager. + let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) + configuration.delegateClass = SceneDelegate.self + return configuration + } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { NSTextAttachment.registerViewProviderClass(PillAttachmentViewProvider.self, forFileType: InfoPlistReader.main.pillsUTType) diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 5169a653a1..9e689a92bf 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -42,6 +42,7 @@ final class AppSettings { case swiftUITimelineEnabled case voiceMessageEnabled case mentionsEnabled + case appLockFlowEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -257,4 +258,7 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.mentionsEnabled, defaultValue: false, storageType: .userDefaults(store)) var mentionsEnabled + + @UserPreference(key: UserDefaultsKeys.appLockFlowEnabled, defaultValue: false, storageType: .volatile) + var appLockFlowEnabled } diff --git a/ElementX/Sources/Application/Application.swift b/ElementX/Sources/Application/Application.swift index 69a576ec99..41a1432b01 100644 --- a/ElementX/Sources/Application/Application.swift +++ b/ElementX/Sources/Application/Application.swift @@ -18,8 +18,9 @@ import SwiftUI @main struct Application: App { - @UIApplicationDelegateAdaptor(AppDelegate.self) private var applicationDelegate + @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate @Environment(\.openURL) private var openURL + private var appCoordinator: AppCoordinatorProtocol! init() { @@ -28,7 +29,9 @@ struct Application: App { } else if ProcessInfo.isRunningUnitTests { appCoordinator = UnitTestsAppCoordinator() } else { - appCoordinator = AppCoordinator(appDelegate: applicationDelegate) + let coordinator = AppCoordinator(appDelegate: appDelegate) + SceneDelegate.windowManager = coordinator.windowManager + appCoordinator = coordinator } } diff --git a/ElementX/Sources/Application/Navigation/NavigationRootCoordinator.swift b/ElementX/Sources/Application/Navigation/NavigationRootCoordinator.swift index 0d074c2bbd..f07e8f1b71 100644 --- a/ElementX/Sources/Application/Navigation/NavigationRootCoordinator.swift +++ b/ElementX/Sources/Application/Navigation/NavigationRootCoordinator.swift @@ -57,13 +57,18 @@ class NavigationRootCoordinator: ObservableObject, CoordinatorProtocol, CustomSt /// Sets or replaces the presented coordinator /// - Parameter coordinator: the coordinator to display - func setRootCoordinator(_ coordinator: (any CoordinatorProtocol)?, dismissalCallback: (() -> Void)? = nil) { - guard let coordinator else { - rootModule = nil - return - } + func setRootCoordinator(_ coordinator: (any CoordinatorProtocol)?, animated: Bool = true, dismissalCallback: (() -> Void)? = nil) { + var transaction = Transaction() + transaction.disablesAnimations = !animated - rootModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) + withTransaction(transaction) { + guard let coordinator else { + rootModule = nil + return + } + + rootModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) + } } /// - dismissalCallback: called when the sheet has been dismissed, programatically or otherwise diff --git a/ElementX/Sources/Application/Windowing/SceneDelegate.swift b/ElementX/Sources/Application/Windowing/SceneDelegate.swift new file mode 100644 index 0000000000..5e7d95829d --- /dev/null +++ b/ElementX/Sources/Application/Windowing/SceneDelegate.swift @@ -0,0 +1,29 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// A basic window scene delegate used to configure the `WindowManager`. +/// +/// We don't support multiple scenes right now, so the implementation is pretty basic. +class SceneDelegate: NSObject, UIWindowSceneDelegate { + weak static var windowManager: WindowManager! + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = scene as? UIWindowScene, !ProcessInfo.isRunningTests else { return } + Self.windowManager.configure(with: windowScene) + } +} diff --git a/ElementX/Sources/Application/Windowing/WindowManager.swift b/ElementX/Sources/Application/Windowing/WindowManager.swift new file mode 100644 index 0000000000..43def9f4b3 --- /dev/null +++ b/ElementX/Sources/Application/Windowing/WindowManager.swift @@ -0,0 +1,67 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +protocol WindowManagerDelegate: AnyObject { + /// The window manager has configured its windows. + func windowManagerDidConfigureWindows(_ windowManager: WindowManager) +} + +@MainActor +/// A window manager that supports switching between a main app window with an overlay and +/// an alternate window to switch contexts whilst also preserving the main view hierarchy. +class WindowManager { + weak var delegate: WindowManagerDelegate? + + /// The app's main window (we only support a single scene). + private(set) var mainWindow: UIWindow! + /// Presented on top of the main window, to display e.g. user indicators. + private(set) var overlayWindow: UIWindow! + /// A secondary window that can be presented instead of the main/overlay window combo. + private(set) var alternateWindow: UIWindow! + + /// Configures the window manager to operate on the supplied scene. + func configure(with windowScene: UIWindowScene) { + mainWindow = windowScene.keyWindow + + overlayWindow = UIWindow(windowScene: windowScene) + overlayWindow.backgroundColor = .clear + // We don't support user interaction on our indicators so disable interaction, to pass + // touches through to the main window. If this changes, there's another solution here: + // https://www.fivestars.blog/articles/swiftui-windows/ + overlayWindow.isUserInteractionEnabled = false + + alternateWindow = UIWindow(windowScene: windowScene) + + delegate?.windowManagerDidConfigureWindows(self) + } + + /// Shows the main and overlay window combo, hiding the alternate window. + func switchToMain() { + mainWindow.isHidden = false + overlayWindow.isHidden = false + alternateWindow.isHidden = true + } + + /// Shows the alternate window, hiding the main and overlay combo. + func switchToAlternate() { + alternateWindow.isHidden = false + overlayWindow.isHidden = true + mainWindow.isHidden = true + } +} diff --git a/ElementX/Sources/FlowCoordinators/AppLockFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/AppLockFlowCoordinator.swift new file mode 100644 index 0000000000..f489c0fcb6 --- /dev/null +++ b/ElementX/Sources/FlowCoordinators/AppLockFlowCoordinator.swift @@ -0,0 +1,87 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +enum AppLockFlowCoordinatorAction: Equatable { + /// Display the unlock flow. + case lockApp + /// Hide the unlock flow. + case unlockApp +} + +/// Coordinates the display of any screens shown when the app is locked. +class AppLockFlowCoordinator: CoordinatorProtocol { + let appLockService: AppLockServiceProtocol + let navigationCoordinator: NavigationRootCoordinator + + private var cancellables: Set = [] + + private let actionsSubject: PassthroughSubject = .init() + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(appLockService: AppLockServiceProtocol, navigationCoordinator: NavigationRootCoordinator) { + self.appLockService = appLockService + self.navigationCoordinator = navigationCoordinator + + NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification) + .sink { [weak self] _ in + self?.showPlaceholderIfNeeded() + } + .store(in: &cancellables) + + NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification) + .sink { [weak self] _ in + self?.showUnlockScreenIfNeeded() + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(navigationCoordinator.toPresentable()) + } + + // MARK: - App unlock + + /// Displays the unlock flow with the app's placeholder view to hide obscure the view hierarchy in the app switcher. + private func showPlaceholderIfNeeded() { + guard appLockService.isEnabled else { return } + + navigationCoordinator.setRootCoordinator(PlaceholderScreenCoordinator(), animated: false) + actionsSubject.send(.lockApp) + } + + /// Displays the unlock flow with the main unlock screen. + private func showUnlockScreenIfNeeded() { + guard appLockService.isEnabled, appLockService.needsUnlock else { return } + + let coordinator = AppLockScreenCoordinator(parameters: .init(appLockService: appLockService)) + coordinator.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .appUnlocked: + actionsSubject.send(.unlockApp) + } + } + .store(in: &cancellables) + + navigationCoordinator.setRootCoordinator(coordinator, animated: false) + actionsSubject.send(.lockApp) + } +} diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index 2fb62c2fab..d274c4113c 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -10,6 +10,12 @@ 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 { + /// Unlock + public static var commonUnlock: String { return UntranslatedL10n.tr("Untranslated", "common_unlock") } + /// %@ is locked + public static func screenAppLockTitle(_ p1: Any) -> String { + return UntranslatedL10n.tr("Untranslated", "screen_app_lock_title", String(describing: p1)) + } /// Clear all data currently stored on this device? /// Sign in again to access your account data and messages. public static var softLogoutClearDataDialogContent: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_content") } diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 97f0c8ce26..7157345557 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -314,6 +314,91 @@ class CompletionSuggestionServiceMock: CompletionSuggestionServiceProtocol { setSuggestionTriggerClosure?(suggestionTrigger) } } +class KeychainControllerMock: KeychainControllerProtocol { + + //MARK: - setRestorationToken + + var setRestorationTokenForUsernameCallsCount = 0 + var setRestorationTokenForUsernameCalled: Bool { + return setRestorationTokenForUsernameCallsCount > 0 + } + var setRestorationTokenForUsernameReceivedArguments: (restorationToken: RestorationToken, forUsername: String)? + var setRestorationTokenForUsernameReceivedInvocations: [(restorationToken: RestorationToken, forUsername: String)] = [] + var setRestorationTokenForUsernameClosure: ((RestorationToken, String) -> Void)? + + func setRestorationToken(_ restorationToken: RestorationToken, forUsername: String) { + setRestorationTokenForUsernameCallsCount += 1 + setRestorationTokenForUsernameReceivedArguments = (restorationToken: restorationToken, forUsername: forUsername) + setRestorationTokenForUsernameReceivedInvocations.append((restorationToken: restorationToken, forUsername: forUsername)) + setRestorationTokenForUsernameClosure?(restorationToken, forUsername) + } + //MARK: - restorationTokenForUsername + + var restorationTokenForUsernameCallsCount = 0 + var restorationTokenForUsernameCalled: Bool { + return restorationTokenForUsernameCallsCount > 0 + } + var restorationTokenForUsernameReceivedUsername: String? + var restorationTokenForUsernameReceivedInvocations: [String] = [] + var restorationTokenForUsernameReturnValue: RestorationToken? + var restorationTokenForUsernameClosure: ((String) -> RestorationToken?)? + + func restorationTokenForUsername(_ username: String) -> RestorationToken? { + restorationTokenForUsernameCallsCount += 1 + restorationTokenForUsernameReceivedUsername = username + restorationTokenForUsernameReceivedInvocations.append(username) + if let restorationTokenForUsernameClosure = restorationTokenForUsernameClosure { + return restorationTokenForUsernameClosure(username) + } else { + return restorationTokenForUsernameReturnValue + } + } + //MARK: - restorationTokens + + var restorationTokensCallsCount = 0 + var restorationTokensCalled: Bool { + return restorationTokensCallsCount > 0 + } + var restorationTokensReturnValue: [KeychainCredentials]! + var restorationTokensClosure: (() -> [KeychainCredentials])? + + func restorationTokens() -> [KeychainCredentials] { + restorationTokensCallsCount += 1 + if let restorationTokensClosure = restorationTokensClosure { + return restorationTokensClosure() + } else { + return restorationTokensReturnValue + } + } + //MARK: - removeRestorationTokenForUsername + + var removeRestorationTokenForUsernameCallsCount = 0 + var removeRestorationTokenForUsernameCalled: Bool { + return removeRestorationTokenForUsernameCallsCount > 0 + } + var removeRestorationTokenForUsernameReceivedUsername: String? + var removeRestorationTokenForUsernameReceivedInvocations: [String] = [] + var removeRestorationTokenForUsernameClosure: ((String) -> Void)? + + func removeRestorationTokenForUsername(_ username: String) { + removeRestorationTokenForUsernameCallsCount += 1 + removeRestorationTokenForUsernameReceivedUsername = username + removeRestorationTokenForUsernameReceivedInvocations.append(username) + removeRestorationTokenForUsernameClosure?(username) + } + //MARK: - removeAllRestorationTokens + + var removeAllRestorationTokensCallsCount = 0 + var removeAllRestorationTokensCalled: Bool { + return removeAllRestorationTokensCallsCount > 0 + } + var removeAllRestorationTokensClosure: (() -> Void)? + + func removeAllRestorationTokens() { + removeAllRestorationTokensCallsCount += 1 + removeAllRestorationTokensClosure?() + } +} class MediaPlayerMock: MediaPlayerProtocol { var mediaSource: MediaSourceProxy? var currentTime: TimeInterval { diff --git a/ElementX/Sources/Screens/AppLockScreen/AppLockScreenCoordinator.swift b/ElementX/Sources/Screens/AppLockScreen/AppLockScreenCoordinator.swift new file mode 100644 index 0000000000..79db0d3040 --- /dev/null +++ b/ElementX/Sources/Screens/AppLockScreen/AppLockScreenCoordinator.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 AppLockScreenCoordinatorParameters { + /// The service used to unlock the app. + let appLockService: AppLockServiceProtocol +} + +enum AppLockScreenCoordinatorAction { + /// The user has successfully unlocked the app. + case appUnlocked +} + +final class AppLockScreenCoordinator: CoordinatorProtocol { + private let parameters: AppLockScreenCoordinatorParameters + private var viewModel: AppLockScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: AppLockScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = AppLockScreenViewModel(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 .appUnlocked: + self.actionsSubject.send(.appUnlocked) + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(AppLockScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/AppLockScreen/AppLockScreenModels.swift b/ElementX/Sources/Screens/AppLockScreen/AppLockScreenModels.swift new file mode 100644 index 0000000000..16f2a5f08d --- /dev/null +++ b/ElementX/Sources/Screens/AppLockScreen/AppLockScreenModels.swift @@ -0,0 +1,40 @@ +// +// 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 + +enum AppLockScreenViewModelAction { + /// The user has successfully unlocked the app. + case appUnlocked +} + +struct AppLockScreenViewState: BindableState { + var bindings: AppLockScreenViewStateBindings +} + +struct AppLockScreenViewStateBindings { } + +enum AppLockScreenViewAction: CustomStringConvertible { + /// Attempt to unlock the app with the supplied PIN code. + case submitPINCode(String) + + var description: String { + switch self { + case .submitPINCode: + return "submitPINCode" + } + } +} diff --git a/ElementX/Sources/Screens/AppLockScreen/AppLockScreenViewModel.swift b/ElementX/Sources/Screens/AppLockScreen/AppLockScreenViewModel.swift new file mode 100644 index 0000000000..732a32debf --- /dev/null +++ b/ElementX/Sources/Screens/AppLockScreen/AppLockScreenViewModel.swift @@ -0,0 +1,51 @@ +// +// 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 AppLockScreenViewModelType = StateStoreViewModel + +class AppLockScreenViewModel: AppLockScreenViewModelType, AppLockScreenViewModelProtocol { + private let appLockService: AppLockServiceProtocol + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(appLockService: AppLockServiceProtocol) { + self.appLockService = appLockService + + super.init(initialViewState: AppLockScreenViewState(bindings: .init())) + } + + // MARK: - Public + + override func process(viewAction: AppLockScreenViewAction) { + MXLog.info("View model: received view action: \(viewAction)") + + switch viewAction { + case .submitPINCode(let pinCode): + guard appLockService.unlock(with: pinCode) else { + MXLog.warning("Invalid PIN code entered.") + // Indicate failure here. + return + } + actionsSubject.send(.appUnlocked) + } + } +} diff --git a/ElementX/Sources/Screens/AppLockScreen/AppLockScreenViewModelProtocol.swift b/ElementX/Sources/Screens/AppLockScreen/AppLockScreenViewModelProtocol.swift new file mode 100644 index 0000000000..4cc2f9fa13 --- /dev/null +++ b/ElementX/Sources/Screens/AppLockScreen/AppLockScreenViewModelProtocol.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 AppLockScreenViewModelProtocol { + var actions: AnyPublisher { get } + var context: AppLockScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/AppLockScreen/View/AppLockScreen.swift b/ElementX/Sources/Screens/AppLockScreen/View/AppLockScreen.swift new file mode 100644 index 0000000000..2718732cdf --- /dev/null +++ b/ElementX/Sources/Screens/AppLockScreen/View/AppLockScreen.swift @@ -0,0 +1,63 @@ +// +// 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 + +// Move this to Compound. +extension ShapeStyle where Self == Color { + static var compound: CompoundColors { Self.compound } +} + +// This implementation is only for development purposes. + +struct AppLockScreen: View { + @ObservedObject var context: AppLockScreenViewModel.Context + + var body: some View { + FullscreenDialog { + VStack(spacing: 8) { + AuthenticationIconImage(image: Image(systemSymbol: .lock)) + .symbolVariant(.fill) + .padding(.bottom, 8) + + Text(UntranslatedL10n.screenAppLockTitle(InfoPlistReader.main.bundleDisplayName)) + .font(.compound.headingMDBold) + .multilineTextAlignment(.center) + .foregroundColor(.compound.textPrimary) + } + } bottomContent: { + Button(UntranslatedL10n.commonUnlock) { + context.send(viewAction: .submitPINCode("0000")) + } + .buttonStyle(.elementAction(.xLarge)) + } + } +} + +// MARK: - Previews + +// Add TestablePreview conformance once we have designs. +struct AppLockScreen_Previews: PreviewProvider { + static let viewModel = AppLockScreenViewModel(appLockService: AppLockService(keychainController: KeychainControllerMock(), + appSettings: ServiceLocator.shared.settings)) + + static var previews: some View { + NavigationStack { + AppLockScreen(context: viewModel.context) + } + } +} diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 9d8822fd85..610bf68d14 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -51,6 +51,7 @@ protocol DeveloperOptionsProtocol: AnyObject { var swiftUITimelineEnabled: Bool { get set } var voiceMessageEnabled: Bool { get set } var mentionsEnabled: Bool { get set } + var appLockFlowEnabled: Bool { get set } } extension AppSettings: DeveloperOptionsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 05b3551307..ea149d3f5f 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -63,6 +63,13 @@ struct DeveloperOptionsScreen: View { Text("Enable voice messages") } } + + Section("Security") { + Toggle(isOn: $context.appLockFlowEnabled) { + Text("PIN/Biometric lock") + Text("Resets on reboot") + } + } Section { Button { diff --git a/ElementX/Sources/Services/AppLock/AppLockService.swift b/ElementX/Sources/Services/AppLock/AppLockService.swift new file mode 100644 index 0000000000..ca4372ab72 --- /dev/null +++ b/ElementX/Sources/Services/AppLock/AppLockService.swift @@ -0,0 +1,55 @@ +// +// 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 + +@MainActor +protocol AppLockServiceProtocol { + /// The app has been configured to automatically lock with a PIN code. + var isEnabled: Bool { get } + /// The app can additionally be unlocked using FaceID or TouchID. + var supportsBiometrics: Bool { get } + /// The app should be unlocked with a PIN code/biometrics before being presented. + var needsUnlock: Bool { get } + + /// 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 +} + +class AppLockService: AppLockServiceProtocol { + private let keychainController: KeychainControllerProtocol + private let appSettings: AppSettings + + var isEnabled: Bool { appSettings.appLockFlowEnabled } + var supportsBiometrics: Bool { true } + var needsUnlock: Bool { true } + + init(keychainController: KeychainControllerProtocol, appSettings: AppSettings) { + self.keychainController = keychainController + self.appSettings = appSettings + } + + func unlock(with pinCode: String) -> Bool { + true + } + + func unlockWithBiometrics() -> Bool { + guard supportsBiometrics else { return false } + return true + } +} diff --git a/ElementX/Sources/Services/Keychain/KeychainControllerMock.swift b/ElementX/Sources/Services/Keychain/KeychainControllerMock.swift new file mode 100644 index 0000000000..e15947427e --- /dev/null +++ b/ElementX/Sources/Services/Keychain/KeychainControllerMock.swift @@ -0,0 +1,23 @@ +// +// 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 MatrixRustSDK + +/// Adds the missing methods for conformance to the protocol. +extension KeychainControllerMock { + func retrieveSessionFromKeychain(userId: String) throws -> Session { fatalError("Not implemented") } + func saveSessionInKeychain(session: Session) { fatalError("Not implemented") } +} diff --git a/ElementX/Sources/Services/Keychain/KeychainControllerProtocol.swift b/ElementX/Sources/Services/Keychain/KeychainControllerProtocol.swift index 51570ce2a2..f804cff105 100644 --- a/ElementX/Sources/Services/Keychain/KeychainControllerProtocol.swift +++ b/ElementX/Sources/Services/Keychain/KeychainControllerProtocol.swift @@ -22,6 +22,7 @@ struct KeychainCredentials { let restorationToken: RestorationToken } +// sourcery: AutoMockable protocol KeychainControllerProtocol: ClientSessionDelegate { func setRestorationToken(_ restorationToken: RestorationToken, forUsername: String) func restorationTokenForUsername(_ username: String) -> RestorationToken? diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index 4792e2f10b..f68c31aa1d 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -33,9 +33,8 @@ class UserSessionStore: UserSessionStoreProtocol { var clientSessionDelegate: ClientSessionDelegate { keychainController } - init(backgroundTaskService: BackgroundTaskServiceProtocol) { - keychainController = KeychainController(service: .sessions, - accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) + init(keychainController: KeychainControllerProtocol, backgroundTaskService: BackgroundTaskServiceProtocol) { + self.keychainController = keychainController self.backgroundTaskService = backgroundTaskService baseDirectory = .sessionsBaseDirectory MXLog.info("Setup base directory at: \(baseDirectory)") diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index a395b5fcf1..67ab0fdf56 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -157,6 +157,10 @@ class MockScreen: Identifiable { let coordinator = TemplateScreenCoordinator(parameters: .init()) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator + case .appLockScreen: + let appLockService = AppLockService(keychainController: KeychainControllerMock(), appSettings: ServiceLocator.shared.settings) + let coordinator = AppLockScreenCoordinator(parameters: .init(appLockService: appLockService)) + return coordinator case .home: let navigationStackCoordinator = NavigationStackCoordinator() let session = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:matrix.org"), diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index 3e4d987af0..00780d9862 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -29,6 +29,7 @@ enum UITestsScreenIdentifier: String { case analyticsSettingsScreen case migration case templateScreen + case appLockScreen case home case settings case bugReport diff --git a/UITests/Sources/AppLockScreenUITests.swift b/UITests/Sources/AppLockScreenUITests.swift new file mode 100644 index 0000000000..7f6bdd9541 --- /dev/null +++ b/UITests/Sources/AppLockScreenUITests.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 AppLockScreenUITests: XCTestCase { + func testScreen() async throws { + let app = Application.launch(.appLockScreen) + try await app.assertScreenshot(.appLockScreen) + } +} diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.appLockScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.appLockScreen.png new file mode 100644 index 0000000000..c18e95c20d --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.appLockScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e09c1a718424e75c120fc2f2038f786388fcb2542231bc737c332e0b23004b33 +size 67281 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.appLockScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.appLockScreen.png new file mode 100644 index 0000000000..545c84d881 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.appLockScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abb591b36f490253c34fd00092e45ba35961ba0aab4d6fcb8efb036ac3c2575e +size 67921 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.appLockScreen.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.appLockScreen.png new file mode 100644 index 0000000000..c70a609049 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.appLockScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89df727aac25c1d26ec86fe564ae97ca1687ba9d710dd5fc234f0052676c0a19 +size 69836 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.appLockScreen.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.appLockScreen.png new file mode 100644 index 0000000000..cfffe95374 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.appLockScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d08f08f1d3e22048432ec4733cd0192e763325760d338864e9395cb24e124f21 +size 72314 diff --git a/UnitTests/Sources/AppLockScreenViewModelTests.swift b/UnitTests/Sources/AppLockScreenViewModelTests.swift new file mode 100644 index 0000000000..566b82338a --- /dev/null +++ b/UnitTests/Sources/AppLockScreenViewModelTests.swift @@ -0,0 +1,46 @@ +// +// 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 AppLockScreenViewModelTests: XCTestCase { + var appLockService: AppLockService! + var viewModel: AppLockScreenViewModelProtocol! + + var context: AppLockScreenViewModelType.Context { viewModel.context } + + override func setUp() { + AppSettings.reset() + appLockService = AppLockService(keychainController: KeychainControllerMock(), appSettings: AppSettings()) + viewModel = AppLockScreenViewModel(appLockService: appLockService) + } + + func testUnlock() async throws { + // Given a valid PIN code. + let pinCode = "0000" + + // When entering it on the lock screen. + let deferred = deferFulfillment(viewModel.actions) { $0 == .appUnlocked } + context.send(viewAction: .submitPINCode(pinCode)) + let result = try await deferred.fulfill() + + // The app should become unlocked. + XCTAssertEqual(result, .appUnlocked) + } +} diff --git a/changelog.d/pr-1876.wip b/changelog.d/pr-1876.wip new file mode 100644 index 0000000000..43372d4d5e --- /dev/null +++ b/changelog.d/pr-1876.wip @@ -0,0 +1 @@ +Initial setup for PIN/Biometric app lock. \ No newline at end of file