diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 7ed79f0ee4..2c45dcc713 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 56; objects = { /* Begin PBXAggregateTarget section */ @@ -344,7 +344,6 @@ 523C6800ED85D5810CF18C19 /* OIDCAccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */; }; 52473A4D7B1FBD4CD1E770C8 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; - 538426B497672A097B212735 /* QRCodeLoginController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A07343F18BB8EAC17B07B7 /* QRCodeLoginController.swift */; }; 53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */; }; 53A59720F4729D9BBFFB7CAB /* NotificationSettingsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9CB3B9DFA353AB2B7CD9F8 /* NotificationSettingsEditScreenCoordinator.swift */; }; 53C1E7F6A7D6409D89F36ED7 /* AggregatedReactionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */; }; @@ -602,7 +601,6 @@ 8ED8AF57A06F5EE9978ED23F /* AuthenticationStartScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB89DC7F9A4A91020037001 /* AuthenticationStartScreenViewModelTests.swift */; }; 8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */; }; 8F2FAA98457750D9D664136F /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; }; - 90067BB37EBA60FA4AEF2DA3 /* QRCodeLoginServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B4ED923603F6110D4960C5 /* QRCodeLoginServiceMock.swift */; }; 904F06C9C1AEF884C2077542 /* RoomDirectorySearchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2E4EF80DFB8FE7C4469B15D /* RoomDirectorySearchScreen.swift */; }; 90733645AE76FB33DAD28C2B /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE40D4A5DD857AC16EED945A /* URLSession.swift */; }; 9095B9E40DB5CF8BA26CE0D8 /* ReactionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 153726EDCE1ACBB3D466A916 /* ReactionsSummaryView.swift */; }; @@ -705,6 +703,8 @@ A6F345328CCC5C9B0DAE2257 /* LogViewerScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BB05221D7D941CC82DC8480 /* LogViewerScreenViewModel.swift */; }; A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */; }; A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; }; + A78DD8692C0766B1005A32A9 /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A78DD8682C0766B1005A32A9 /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */; }; + A78DD86B2C076B3F005A32A9 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = A78DD86A2C076B3F005A32A9 /* Data.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 */; }; @@ -837,6 +837,7 @@ C85C7A201E4CFDA477ACEBEB /* AppLockSetupSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8610C1D21565C950BCA6A454 /* AppLockSetupSettingsScreenViewModelProtocol.swift */; }; C8A9C595038AFA2D707AC8C1 /* NotificationPermissionsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E69F67D2A70ABD08CA6D54 /* NotificationPermissionsScreenViewModelProtocol.swift */; }; C8BD80891BAD688EF2C15CDB /* MediaUploadPreviewScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DD0855F2F76D47E5555082 /* MediaUploadPreviewScreenCoordinator.swift */; }; + C8C7AF33AADF88B306CD2695 /* QRCodeLoginService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4427AF4B7FB7EF3E3D424C7 /* QRCodeLoginService.swift */; }; C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */; }; C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E0B4A34E69BD2132BEC521 /* MessageText.swift */; }; C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81B6170DB690013CEB646F4 /* MapLibreModels.swift */; }; @@ -858,6 +859,7 @@ CDAE3A37D4DF136F9D07DB61 /* RoomChangeRolesScreenSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF710CB1C31F8938EAA3A7D /* RoomChangeRolesScreenSection.swift */; }; CDCA8A559E098503DDE29477 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; }; CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; }; + CE3B7FC34FB2C279AAA5EA01 /* AVMetadataMachineReadableCodeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3339B1DDB1341E833D2555BC /* AVMetadataMachineReadableCodeObject.swift */; }; CE6F237360875D3D573FD0B2 /* RoomNotificationSettingsProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6B522BD637845AB9570B10 /* RoomNotificationSettingsProxy.swift */; }; CE9530A4CA661E090635C2F2 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; }; CEAEA57B7665C8E790599A78 /* BlockedUsersScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 240610DF32F3213BEC5611D7 /* BlockedUsersScreenViewModelTests.swift */; }; @@ -941,6 +943,7 @@ E313BDD2B8813144139B2E00 /* UserDiscoveryServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0287793F11C480E242B03DF5 /* UserDiscoveryServiceTest.swift */; }; E3291AD16D7A5CB14781819C /* UserNotificationCenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D8149FDDA0315CDC553B4B /* UserNotificationCenterProtocol.swift */; }; E32A18802EB37EEE3EF7B965 /* GlobalSearchScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B788615712FED326F73D3F83 /* GlobalSearchScreenViewModelProtocol.swift */; }; + E362924A42934C9F0F97A956 /* OIDCConfigurationProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69869844D2B6F5BD9AABF85 /* OIDCConfigurationProxy.swift */; }; E37044401D9951D6C02C0855 /* TracingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */; }; E3AC72E3E58F364EF15C1CC7 /* NotificationSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514363244AE7D68080D44C6F /* NotificationSettingsScreenViewModelTests.swift */; }; E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */; }; @@ -1136,12 +1139,12 @@ 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 = ""; }; - 0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = ""; }; + 0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_voice_message.m4a; 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 = ""; }; 044E501B8331B339874D1B96 /* CompoundIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompoundIcon.swift; sourceTree = ""; }; 045253F9967A535EE5B16691 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = ""; }; - 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityIdentifiers.swift; sourceTree = ""; }; 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; 0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxyProtocol.swift; sourceTree = ""; }; @@ -1200,7 +1203,7 @@ 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = ""; }; 128501375217576AF0FE3E92 /* RoomAttachmentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAttachmentPicker.swift; sourceTree = ""; }; 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.swift; sourceTree = ""; }; - 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; + 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 136F80A613B55BDD071DCEA5 /* JoinRoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenModels.swift; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1288,7 +1291,7 @@ 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = ""; }; 25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenCoordinator.swift; sourceTree = ""; }; 260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = ""; }; - 267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; path = PreviewTests.xctestplan; sourceTree = ""; }; + 267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PreviewTests.xctestplan; sourceTree = ""; }; 26B0A96B8FE4849227945067 /* VoiceMessageRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorder.swift; sourceTree = ""; }; 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceProtocol.swift; sourceTree = ""; }; 2721D7B051F0159AA919DA05 /* RoomChangePermissionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1333,6 +1336,7 @@ 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyProtocol.swift; sourceTree = ""; }; 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelTests.swift; sourceTree = ""; }; 330AF4D121C3396F7A14B21D /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/SAS.strings; sourceTree = ""; }; + 3339B1DDB1341E833D2555BC /* AVMetadataMachineReadableCodeObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVMetadataMachineReadableCodeObject.swift; sourceTree = ""; }; 3368395F06AA180138E185B6 /* PollFormScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenUITests.swift; sourceTree = ""; }; 33AE897D86784CCA5E4E9227 /* ElementCallService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallService.swift; sourceTree = ""; }; 33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; @@ -1346,7 +1350,7 @@ 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 = ""; }; 35FA991289149D31F4286747 /* UserPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreference.swift; sourceTree = ""; }; - 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyMock.swift; sourceTree = ""; }; 376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerURLBuildersTests.swift; sourceTree = ""; }; 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItem.swift; sourceTree = ""; }; @@ -1625,7 +1629,6 @@ 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceProtocol.swift; sourceTree = ""; }; 7FB2253D36E81E045E1CB432 /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = ""; }; 7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenModels.swift; sourceTree = ""; }; - 80A07343F18BB8EAC17B07B7 /* QRCodeLoginController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginController.swift; sourceTree = ""; }; 80C4927D09099497233E9980 /* WaitlistScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreen.swift; sourceTree = ""; }; 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageEventStringBuilder.swift; sourceTree = ""; }; 8166F121C79C7B62BF01D508 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pt; path = pt.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1681,7 +1684,7 @@ 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; 8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = ""; }; 8F6210134203BE1F2DD5C679 /* RoomDirectoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectoryCell.swift; sourceTree = ""; }; 8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenViewModelTests.swift; sourceTree = ""; }; @@ -1690,7 +1693,6 @@ 90791B9C739C716A40E1B230 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 907FA4DE17DEA1A3738EFB83 /* AudioRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorder.swift; sourceTree = ""; }; 90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; - 90B4ED923603F6110D4960C5 /* QRCodeLoginServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginServiceMock.swift; sourceTree = ""; }; 90DFF217B3D9D0941283278C /* RoomRolesAndPermissionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenViewModelProtocol.swift; sourceTree = ""; }; 90F2F8998E5632668B0AD848 /* RoomTimelineItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemView.swift; sourceTree = ""; }; 913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachmentData.swift; sourceTree = ""; }; @@ -1767,9 +1769,12 @@ A436057DBEA1A23CA8CB1FD7 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = ""; }; A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetectionTests.swift; sourceTree = ""; }; A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = ""; }; + A69869844D2B6F5BD9AABF85 /* OIDCConfigurationProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCConfigurationProxy.swift; sourceTree = ""; }; A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = ""; }; A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriverProtocol.swift; sourceTree = ""; }; A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = ""; }; + A78DD8682C0766B1005A32A9 /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVMetadataMachineReadableCodeObjectExtensionsTest.swift; sourceTree = ""; }; + A78DD86A2C076B3F005A32A9 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = ""; }; A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = ""; }; A84D413BF49F0E980F010A6B /* LogViewerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenCoordinator.swift; sourceTree = ""; }; @@ -1830,13 +1835,14 @@ B40233F2989AD49906BB310D /* RoomPollsHistoryScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelTests.swift; sourceTree = ""; }; B410B32B72C90BF94E481F33 /* AppLockSetupPINScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenModels.swift; sourceTree = ""; }; B43456E73F8A2D52B69B9FB9 /* TemplateScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModel.swift; sourceTree = ""; }; + B4427AF4B7FB7EF3E3D424C7 /* QRCodeLoginService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginService.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 = ""; }; B50F03079F6B5EF9CA005F14 /* TimelineProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProxyProtocol.swift; sourceTree = ""; }; B53AC78E49A297AC1D72A7CF /* AppMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediator.swift; sourceTree = ""; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; - B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = ""; }; B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = ""; }; B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = ""; }; B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetails.swift; sourceTree = ""; }; @@ -1947,7 +1953,7 @@ CE47A97726F0675DEE387BF9 /* TypingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = ""; }; CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; CEE20623EB4A9B88FB29F2BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SAS.strings; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2070,7 +2076,7 @@ ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModel.swift; sourceTree = ""; }; - ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = ""; }; ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = ""; }; EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; @@ -2093,7 +2099,7 @@ F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = ""; }; F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = ""; }; - F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = ""; }; + F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = ""; }; F2E4EF80DFB8FE7C4469B15D /* RoomDirectorySearchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreen.swift; sourceTree = ""; }; F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = ""; }; F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = ""; }; @@ -2659,7 +2665,6 @@ 382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */, B2AD8A56CD37E23071A2F4BF /* PHGPostHogMock.swift */, D38391154120264910D19528 /* PollMock.swift */, - 90B4ED923603F6110D4960C5 /* QRCodeLoginServiceMock.swift */, 894EE8F5B399A165BA2A6634 /* RoomDirectorySearchMock.swift */, 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */, F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */, @@ -2977,6 +2982,7 @@ 3E93A1BE7D8A2EBCAD51EEB4 /* Array.swift */, A73A07BAEDD74C48795A996A /* AsyncSequence.swift */, 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */, + 3339B1DDB1341E833D2555BC /* AVMetadataMachineReadableCodeObject.swift */, B6E89E530A8E92EC44301CA1 /* Bundle.swift */, A9FAFE1C2149E6AC8156ED2B /* Collection.swift */, E2B1CC9AA154F4D5435BF60A /* Comparable.swift */, @@ -3007,6 +3013,7 @@ AE40D4A5DD857AC16EED945A /* URLSession.swift */, 897DF5E9A70CE05A632FC8AF /* UTType.swift */, E992D7B8BE54B2AB454613AF /* XCUIElement.swift */, + A78DD86A2C076B3F005A32A9 /* Data.swift */, ); path = Extensions; sourceTree = ""; @@ -3474,7 +3481,7 @@ 70CC0CDA4AFDF8299C56ADE7 /* QRCode */ = { isa = PBXGroup; children = ( - 80A07343F18BB8EAC17B07B7 /* QRCodeLoginController.swift */, + B4427AF4B7FB7EF3E3D424C7 /* QRCodeLoginService.swift */, 536C0E2178949B290776EA4E /* QRCodeLoginServiceProtocol.swift */, ); path = QRCode; @@ -3600,6 +3607,7 @@ 7583EAC171059A86B767209F /* MediaProvider */, 7DBC911559934065993A5FF4 /* NotificationManager */, 1C62F5382CC9D9F7DCEC344A /* UserDiscoveryService */, + A78DD8682C0766B1005A32A9 /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */, ); path = Sources; sourceTree = ""; @@ -4338,6 +4346,7 @@ CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */, 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */, 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */, + A69869844D2B6F5BD9AABF85 /* OIDCConfigurationProxy.swift */, ); path = Authentication; sourceTree = ""; @@ -5767,6 +5776,7 @@ CC0D088F505F33A20DC5590F /* RoomStateEventStringBuilderTests.swift in Sources */, 7691233E3572A9173FD96CB3 /* SecureBackupKeyBackupScreenViewModelTests.swift in Sources */, EB87DF90CF6F8D5D12404C6E /* SecureBackupLogoutConfirmationScreenViewModelTests.swift in Sources */, + A78DD8692C0766B1005A32A9 /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift in Sources */, 06B31F84CE52A7A7C271267C /* SecureBackupRecoveryKeyScreenViewModelTests.swift in Sources */, 1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */, 53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */, @@ -5812,6 +5822,7 @@ buildActionMask = 2147483647; files = ( 41F553349AF44567184822D8 /* APNSPayload.swift in Sources */, + CE3B7FC34FB2C279AAA5EA01 /* AVMetadataMachineReadableCodeObject.swift in Sources */, 70394ECD2DCC70741538620D /* AccessibilityIdentifiers.swift in Sources */, 34433A509DFEC93579B3B35B /* AdvancedSettingsScreen.swift in Sources */, 4557192F5B15A8D9BB920232 /* AdvancedSettingsScreenCoordinator.swift in Sources */, @@ -6058,6 +6069,7 @@ BFEB24336DFD5F196E6F3456 /* IntentionalMentions.swift in Sources */, 2AAB2A77F1762A2648078A30 /* InteractiveQuickLook.swift in Sources */, C4D2BCAA54E2C62B94B24AF4 /* InviteUsersScreen.swift in Sources */, + A78DD86B2C076B3F005A32A9 /* Data.swift in Sources */, E27C4D1A1F8BB77CA790B403 /* InviteUsersScreenCoordinator.swift in Sources */, C26DB49C06C00B5DF1A991A5 /* InviteUsersScreenModels.swift in Sources */, 61941DEE5F3834765770BE01 /* InviteUsersScreenSelectedItem.swift in Sources */, @@ -6185,6 +6197,7 @@ CBD2ABE4C1A47ECD99E1488E /* NotificationSettingsScreenViewModelProtocol.swift in Sources */, 523C6800ED85D5810CF18C19 /* OIDCAccountSettingsPresenter.swift in Sources */, 9A4E3D5AA44B041DAC3A0D81 /* OIDCAuthenticationPresenter.swift in Sources */, + E362924A42934C9F0F97A956 /* OIDCConfigurationProxy.swift in Sources */, 11A6B8E3CBDBF0A4107FF4CE /* OnboardingFlowCoordinator.swift in Sources */, 3CE4C5071B6D2576E2473989 /* OrderedSet.swift in Sources */, AA5924D3B67F7ACD98BBEFDC /* OrientationManagerProtocol.swift in Sources */, @@ -6224,13 +6237,12 @@ C7ABEBECDC513F7887DACF66 /* ProgressMaskModifier.swift in Sources */, 9B356742E035D90A8BB5CABE /* ProposedViewSize.swift in Sources */, 2835FD52F3F618D07F799B3D /* Publisher.swift in Sources */, - 538426B497672A097B212735 /* QRCodeLoginController.swift in Sources */, 4949C8C12669D1B5E082366E /* QRCodeLoginScreen.swift in Sources */, 4D23D41B8109E010304050F8 /* QRCodeLoginScreenCoordinator.swift in Sources */, 46FCD999E92D9717D24AAB94 /* QRCodeLoginScreenModels.swift in Sources */, 30E5628F74AD3C27A061BF25 /* QRCodeLoginScreenViewModel.swift in Sources */, E9D2ED1C4186931E3D5FDA4E /* QRCodeLoginScreenViewModelProtocol.swift in Sources */, - 90067BB37EBA60FA4AEF2DA3 /* QRCodeLoginServiceMock.swift in Sources */, + C8C7AF33AADF88B306CD2695 /* QRCodeLoginService.swift in Sources */, BB04B1D8E7401C90506D401E /* QRCodeLoginServiceProtocol.swift in Sources */, FDD5B4B616D9FF4DE3E9A418 /* QRCodeScannerView.swift in Sources */, 9095B9E40DB5CF8BA26CE0D8 /* ReactionsSummaryView.swift in Sources */, @@ -6786,9 +6798,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_NSE", - ); + OTHER_SWIFT_FLAGS = "-DIS_NSE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; @@ -6837,9 +6847,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_MAIN_APP", - ); + OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -6865,9 +6873,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_MAIN_APP", - ); + OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -7112,9 +7118,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_NSE", - ); + OTHER_SWIFT_FLAGS = "-DIS_NSE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; @@ -7323,7 +7327,7 @@ repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 1.0.4; + version = 1.0.6; }; }; 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 518b568fbb..176dd35791 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -139,8 +139,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/matrix-rust-components-swift", "state" : { - "revision" : "100598671d3e6186f77e2f48630e9cbcb63fd86b", - "version" : "1.0.4" + "revision" : "510689e69b36de3c6bb8313b92f2709e58d2e80f", + "version" : "1.0.6" } }, { diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 5128326db7..10d549c26d 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -500,6 +500,18 @@ "screen_qr_code_login_connection_note_secure_state_title" = "Connection not secure"; "screen_qr_code_login_device_code_subtitle" = "You’ll be asked to enter the two digits shown on this device."; "screen_qr_code_login_device_code_title" = "Enter the number below on your other device"; +"screen_qr_code_login_device_not_signed_in_scan_state_description" = "Sign in to your other device and then try again, or use another device that’s already signed in."; +"screen_qr_code_login_device_not_signed_in_scan_state_subtitle" = "Other device not signed in"; +"screen_qr_code_login_error_cancelled_subtitle" = "The sign in was cancelled on the other device."; +"screen_qr_code_login_error_cancelled_title" = "Sign in request cancelled"; +"screen_qr_code_login_error_declined_subtitle" = "The request on your other device was not accepted."; +"screen_qr_code_login_error_declined_title" = "Sign in declined"; +"screen_qr_code_login_error_expired_subtitle" = "Sign in expired. Please try again."; +"screen_qr_code_login_error_expired_title" = "The sign in was not completed in time"; +"screen_qr_code_login_error_linking_not_suported_subtitle" = "Your other device does not support signing in to %@ with a QR code.\n\nTry signing in manually, or scan the QR code with another device."; +"screen_qr_code_login_error_linking_not_suported_title" = "QR code not supported"; +"screen_qr_code_login_error_sliding_sync_not_supported_subtitle" = "Your account provider does not support %1$@."; +"screen_qr_code_login_error_sliding_sync_not_supported_title" = "%1$@ not supported"; "screen_qr_code_login_initial_state_button_title" = "Ready to scan"; "screen_qr_code_login_initial_state_item_1" = "Open %1$@ on a desktop device"; "screen_qr_code_login_initial_state_item_2" = "Click on your avatar"; diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 5c3c1ecb94..5eb5570e71 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -444,10 +444,16 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg } private func startAuthentication() { + let encryptionKeyProvider = EncryptionKeyProvider() let authenticationService = AuthenticationServiceProxy(userSessionStore: userSessionStore, - encryptionKeyProvider: EncryptionKeyProvider(), + encryptionKeyProvider: encryptionKeyProvider, appSettings: appSettings) + let qrCodeLoginService = QRCodeLoginService(oidcConfiguration: appSettings.oidcConfiguration.rustValue, + encryptionKeyProvider: encryptionKeyProvider, + userSessionStore: userSessionStore) + authenticationFlowCoordinator = AuthenticationFlowCoordinator(authenticationService: authenticationService, + qrCodeLoginService: qrCodeLoginService, bugReportService: ServiceLocator.shared.bugReportService, navigationRootCoordinator: navigationRootCoordinator, appMediator: appMediator, diff --git a/ElementX/Sources/Application/AppMediator.swift b/ElementX/Sources/Application/AppMediator.swift index 2d5ad199fa..c836d12c4a 100644 --- a/ElementX/Sources/Application/AppMediator.swift +++ b/ElementX/Sources/Application/AppMediator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import AVFoundation import UIKit class AppMediator: AppMediatorProtocol { @@ -64,4 +65,22 @@ class AppMediator: AppMediatorProtocol { func setIdleTimerDisabled(_ disabled: Bool) { application.isIdleTimerDisabled = disabled } + + func requestAuthorizationIfNeeded() async -> Bool { + let status = AVCaptureDevice.authorizationStatus(for: .video) + + // Determine if the user previously authorized camera access. + if status == .authorized { + return true + } + + var isAuthorized = false + // If the system hasn't determined the user's authorization status, + // explicitly prompt them for approval. + if status == .notDetermined { + isAuthorized = await AVCaptureDevice.requestAccess(for: .video) + } + + return isAuthorized + } } diff --git a/ElementX/Sources/Application/AppMediatorProtocol.swift b/ElementX/Sources/Application/AppMediatorProtocol.swift index bff568e146..58fe34041c 100644 --- a/ElementX/Sources/Application/AppMediatorProtocol.swift +++ b/ElementX/Sources/Application/AppMediatorProtocol.swift @@ -33,6 +33,8 @@ protocol AppMediatorProtocol { func openAppSettings() func setIdleTimerDisabled(_ disabled: Bool) + + func requestAuthorizationIfNeeded() async -> Bool } extension UIApplication.State: CustomStringConvertible { diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 5bc16bf763..a376719eb5 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -154,6 +154,15 @@ final class AppSettings { return url }() + private(set) lazy var oidcConfiguration = OIDCConfigurationProxy(clientName: InfoPlistReader.main.bundleDisplayName, + redirectURI: oidcRedirectURL, + clientURI: websiteURL, + logoURI: logoURL, + tosURI: acceptableUseURL, + policyURI: privacyURL, + contacts: [supportEmailAddress], + staticRegistrations: oidcStaticRegistrations.mapKeys { $0.absoluteString }) + /// A dictionary of accounts that have performed an initial sync through their proxy. /// /// This is a temporary workaround. In the future we should be able to receive a signal from the diff --git a/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift index e8f860faea..66abdbb0ba 100644 --- a/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift @@ -31,6 +31,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { private let appSettings: AppSettings private let analytics: AnalyticsService private let userIndicatorController: UserIndicatorControllerProtocol + private let qrCodeLoginService: QRCodeLoginServiceProtocol private var cancellables = Set() @@ -42,6 +43,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { weak var delegate: AuthenticationFlowCoordinatorDelegate? init(authenticationService: AuthenticationServiceProxyProtocol, + qrCodeLoginService: QRCodeLoginServiceProtocol, bugReportService: BugReportServiceProtocol, navigationRootCoordinator: NavigationRootCoordinator, appMediator: AppMediatorProtocol, @@ -55,6 +57,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { self.appSettings = appSettings self.analytics = analytics self.userIndicatorController = userIndicatorController + self.qrCodeLoginService = qrCodeLoginService navigationStackCoordinator = NavigationStackCoordinator() } @@ -106,7 +109,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { } private func startQRCodeLogin() { - let coordinator = QRCodeLoginScreenCoordinator(parameters: .init(qrCodeLoginService: QRCodeLoginService(), + let coordinator = QRCodeLoginScreenCoordinator(parameters: .init(qrCodeLoginService: qrCodeLoginService, orientationManager: appMediator.windowManager, appMediator: appMediator)) coordinator.actionsPublisher.sink { [weak self] action in @@ -114,8 +117,18 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { return } switch action { + case .signInManually: + navigationStackCoordinator.setSheetCoordinator(nil) + Task { await self.startAuthentication() } case .cancel: navigationStackCoordinator.setSheetCoordinator(nil) + case .done(let userSession): + navigationStackCoordinator.setSheetCoordinator(nil) + // Since the qr code login flow includes verification + appSettings.hasRunIdentityConfirmationOnboarding = true + DispatchQueue.main.async { + self.userHasSignedIn(userSession: userSession) + } } } .store(in: &cancellables) diff --git a/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift index 1df12de344..258bd8fc42 100644 --- a/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift @@ -87,9 +87,9 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { configureStateMachine() - stateMachine.tryEvent(.next) - rootNavigationStackCoordinator.setFullScreenCoverCoordinator(navigationStackCoordinator, animated: !isNewLogin) + + stateMachine.tryEvent(.next) } func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { @@ -134,6 +134,8 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { return .analyticsPrompt case (.initial, false, false, false, true): return .notificationPermissions + case (.initial, false, false, false, false): + return .finished case (.identityConfirmation, _, _, _, _): return .identityConfirmed diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 6a995be6ec..6d98637284 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -1221,6 +1221,38 @@ internal enum L10n { internal static var screenQrCodeLoginDeviceCodeSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_device_code_subtitle") } /// Enter the number below on your other device internal static var screenQrCodeLoginDeviceCodeTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_device_code_title") } + /// Sign in to your other device and then try again, or use another device that’s already signed in. + internal static var screenQrCodeLoginDeviceNotSignedInScanStateDescription: String { return L10n.tr("Localizable", "screen_qr_code_login_device_not_signed_in_scan_state_description") } + /// Other device not signed in + internal static var screenQrCodeLoginDeviceNotSignedInScanStateSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_device_not_signed_in_scan_state_subtitle") } + /// The sign in was cancelled on the other device. + internal static var screenQrCodeLoginErrorCancelledSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_cancelled_subtitle") } + /// Sign in request cancelled + internal static var screenQrCodeLoginErrorCancelledTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_cancelled_title") } + /// The request on your other device was not accepted. + internal static var screenQrCodeLoginErrorDeclinedSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_declined_subtitle") } + /// Sign in declined + internal static var screenQrCodeLoginErrorDeclinedTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_declined_title") } + /// Sign in expired. Please try again. + internal static var screenQrCodeLoginErrorExpiredSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_expired_subtitle") } + /// The sign in was not completed in time + internal static var screenQrCodeLoginErrorExpiredTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_expired_title") } + /// Your other device does not support signing in to %@ with a QR code. + /// + /// Try signing in manually, or scan the QR code with another device. + internal static func screenQrCodeLoginErrorLinkingNotSuportedSubtitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_qr_code_login_error_linking_not_suported_subtitle", String(describing: p1)) + } + /// QR code not supported + internal static var screenQrCodeLoginErrorLinkingNotSuportedTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_linking_not_suported_title") } + /// Your account provider does not support %1$@. + internal static func screenQrCodeLoginErrorSlidingSyncNotSupportedSubtitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_qr_code_login_error_sliding_sync_not_supported_subtitle", String(describing: p1)) + } + /// %1$@ not supported + internal static func screenQrCodeLoginErrorSlidingSyncNotSupportedTitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_qr_code_login_error_sliding_sync_not_supported_title", String(describing: p1)) + } /// Ready to scan internal static var screenQrCodeLoginInitialStateButtonTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_initial_state_button_title") } /// Open %1$@ on a desktop device diff --git a/ElementX/Sources/Mocks/AppMediatorMock.swift b/ElementX/Sources/Mocks/AppMediatorMock.swift index 1e1a2db3c7..6f940660c3 100644 --- a/ElementX/Sources/Mocks/AppMediatorMock.swift +++ b/ElementX/Sources/Mocks/AppMediatorMock.swift @@ -17,10 +17,11 @@ import UIKit extension AppMediatorMock { - static var `default`: AppMediatorProtocol { + static var `default`: AppMediatorMock { let mock = AppMediatorMock() mock.underlyingAppState = .active + mock.requestAuthorizationIfNeededUnderlyingReturnValue = true mock.underlyingWindowManager = WindowManagerMock() return mock diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 15973be29e..5c664ea3f2 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1014,6 +1014,70 @@ class AppMediatorMock: AppMediatorProtocol { setIdleTimerDisabledReceivedInvocations.append(disabled) setIdleTimerDisabledClosure?(disabled) } + //MARK: - requestAuthorizationIfNeeded + + var requestAuthorizationIfNeededUnderlyingCallsCount = 0 + var requestAuthorizationIfNeededCallsCount: Int { + get { + if Thread.isMainThread { + return requestAuthorizationIfNeededUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = requestAuthorizationIfNeededUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + requestAuthorizationIfNeededUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + requestAuthorizationIfNeededUnderlyingCallsCount = newValue + } + } + } + } + var requestAuthorizationIfNeededCalled: Bool { + return requestAuthorizationIfNeededCallsCount > 0 + } + + var requestAuthorizationIfNeededUnderlyingReturnValue: Bool! + var requestAuthorizationIfNeededReturnValue: Bool! { + get { + if Thread.isMainThread { + return requestAuthorizationIfNeededUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = requestAuthorizationIfNeededUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + requestAuthorizationIfNeededUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + requestAuthorizationIfNeededUnderlyingReturnValue = newValue + } + } + } + } + var requestAuthorizationIfNeededClosure: (() async -> Bool)? + + func requestAuthorizationIfNeeded() async -> Bool { + requestAuthorizationIfNeededCallsCount += 1 + if let requestAuthorizationIfNeededClosure = requestAuthorizationIfNeededClosure { + return await requestAuthorizationIfNeededClosure() + } else { + return requestAuthorizationIfNeededReturnValue + } + } } class AudioConverterMock: AudioConverterProtocol { @@ -7203,18 +7267,23 @@ class PollInteractionHandlerMock: PollInteractionHandlerProtocol { } } class QRCodeLoginServiceMock: QRCodeLoginServiceProtocol { + var qrLoginProgressPublisher: AnyPublisher { + get { return underlyingQrLoginProgressPublisher } + set(value) { underlyingQrLoginProgressPublisher = value } + } + var underlyingQrLoginProgressPublisher: AnyPublisher! - //MARK: - requestAuthorizationIfNeeded + //MARK: - loginWithQRCode - var requestAuthorizationIfNeededUnderlyingCallsCount = 0 - var requestAuthorizationIfNeededCallsCount: Int { + var loginWithQRCodeDataUnderlyingCallsCount = 0 + var loginWithQRCodeDataCallsCount: Int { get { if Thread.isMainThread { - return requestAuthorizationIfNeededUnderlyingCallsCount + return loginWithQRCodeDataUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = requestAuthorizationIfNeededUnderlyingCallsCount + returnValue = loginWithQRCodeDataUnderlyingCallsCount } return returnValue! @@ -7222,27 +7291,29 @@ class QRCodeLoginServiceMock: QRCodeLoginServiceProtocol { } set { if Thread.isMainThread { - requestAuthorizationIfNeededUnderlyingCallsCount = newValue + loginWithQRCodeDataUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - requestAuthorizationIfNeededUnderlyingCallsCount = newValue + loginWithQRCodeDataUnderlyingCallsCount = newValue } } } } - var requestAuthorizationIfNeededCalled: Bool { - return requestAuthorizationIfNeededCallsCount > 0 + var loginWithQRCodeDataCalled: Bool { + return loginWithQRCodeDataCallsCount > 0 } + var loginWithQRCodeDataReceivedData: Data? + var loginWithQRCodeDataReceivedInvocations: [Data] = [] - var requestAuthorizationIfNeededUnderlyingReturnValue: Bool! - var requestAuthorizationIfNeededReturnValue: Bool! { + var loginWithQRCodeDataUnderlyingReturnValue: Result! + var loginWithQRCodeDataReturnValue: Result! { get { if Thread.isMainThread { - return requestAuthorizationIfNeededUnderlyingReturnValue + return loginWithQRCodeDataUnderlyingReturnValue } else { - var returnValue: Bool? = nil + var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = requestAuthorizationIfNeededUnderlyingReturnValue + returnValue = loginWithQRCodeDataUnderlyingReturnValue } return returnValue! @@ -7250,22 +7321,24 @@ class QRCodeLoginServiceMock: QRCodeLoginServiceProtocol { } set { if Thread.isMainThread { - requestAuthorizationIfNeededUnderlyingReturnValue = newValue + loginWithQRCodeDataUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - requestAuthorizationIfNeededUnderlyingReturnValue = newValue + loginWithQRCodeDataUnderlyingReturnValue = newValue } } } } - var requestAuthorizationIfNeededClosure: (() async -> Bool)? + var loginWithQRCodeDataClosure: ((Data) async -> Result)? - func requestAuthorizationIfNeeded() async -> Bool { - requestAuthorizationIfNeededCallsCount += 1 - if let requestAuthorizationIfNeededClosure = requestAuthorizationIfNeededClosure { - return await requestAuthorizationIfNeededClosure() + func loginWithQRCode(data: Data) async -> Result { + loginWithQRCodeDataCallsCount += 1 + loginWithQRCodeDataReceivedData = data + loginWithQRCodeDataReceivedInvocations.append(data) + if let loginWithQRCodeDataClosure = loginWithQRCodeDataClosure { + return await loginWithQRCodeDataClosure(data) } else { - return requestAuthorizationIfNeededReturnValue + return loginWithQRCodeDataReturnValue } } } diff --git a/ElementX/Sources/Mocks/QRCodeLoginServiceMock.swift b/ElementX/Sources/Mocks/QRCodeLoginServiceMock.swift deleted file mode 100644 index d0950a93f5..0000000000 --- a/ElementX/Sources/Mocks/QRCodeLoginServiceMock.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright 2024 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 - -struct QRCodeLoginServiceMockConfiguration { - var isAuthorized = true -} - -extension QRCodeLoginServiceMock { - convenience init(configuration: QRCodeLoginServiceMockConfiguration) { - self.init() - requestAuthorizationIfNeededReturnValue = configuration.isAuthorized - } -} diff --git a/ElementX/Sources/Other/Extensions/AVMetadataMachineReadableCodeObject.swift b/ElementX/Sources/Other/Extensions/AVMetadataMachineReadableCodeObject.swift new file mode 100644 index 0000000000..a3072ffa0f --- /dev/null +++ b/ElementX/Sources/Other/Extensions/AVMetadataMachineReadableCodeObject.swift @@ -0,0 +1,156 @@ +// +// Copyright 2024 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. +// + +// Helpers to remove ECI headers from QR Code raw data +// https://gist.github.com/PetrusM/267e2ee8c1d8b5dca17eac085afa7d7c +import AVKit +import Foundation + +extension AVMetadataMachineReadableCodeObject { + var binaryValue: Data? { + switch type { + case .qr: + guard let binaryValueWithProtocol, + let symbolVersion = (descriptor as? CIQRCodeDescriptor)?.symbolVersion else { + return nil + } + MXLog.info("QR code raw data: \(binaryValueWithProtocol.map { String(format: "%02x", $0) }.joined())") + MXLog.info("QR code symbol version: \(symbolVersion)") + return Self.removeQrProtocolData(binaryValueWithProtocol, symbolVersion: symbolVersion) + case .aztec: + guard let string = stringValue + else { return nil } + return string.data(using: String.Encoding.isoLatin1) + default: + return nil + } + } + + var binaryValueWithProtocol: Data? { + guard let descriptor else { + return nil + } + switch type { + case .qr: + return (descriptor as? CIQRCodeDescriptor)?.errorCorrectedPayload + case .aztec: + return (descriptor as? CIAztecCodeDescriptor)?.errorCorrectedPayload + case .pdf417: + return (descriptor as? CIPDF417CodeDescriptor)?.errorCorrectedPayload + case .dataMatrix: + return (descriptor as? CIDataMatrixCodeDescriptor)?.errorCorrectedPayload + default: + return nil + } + } + + static func removeQrProtocolData(_ input: Data, symbolVersion: Int) -> Data? { + var halves = input.halfBytes() + var batch = takeBatch(&halves, version: symbolVersion) + var output = batch + while !batch.isEmpty { + batch = takeBatch(&halves, version: symbolVersion) + output.append(contentsOf: batch) + } + let data = Data(output) + MXLog.info("QR code decoded binary data: \(data.map { String(format: "%02x", $0) }.joined())") + return data + } + + private static func takeBatch(_ input: inout [HalfByte], version: Int) -> [UInt8] { + let characterCountLength = version > 9 ? 16 : 8 + let mode = input.remove(at: 0) + var output = [UInt8]() + switch mode.value { + // If there is not only binary in the QRCode, then cases should be added here. + case 0x04: // Binary + let charactersCount: UInt16 + if characterCountLength == 8 { + charactersCount = UInt16(input.takeUInt8()) + } else { + charactersCount = UInt16(input.takeUInt16()) + } + for _ in 0.. UInt8 { + let left = remove(at: 0) + let right = remove(at: 0) + return UInt8(left, right) + } + + mutating func takeUInt16() -> UInt16 { + let first = remove(at: 0) + let second = remove(at: 0) + let third = remove(at: 0) + let fourth = remove(at: 0) + return UInt16(first, second, third, fourth) + } +} + +private extension Data { + func halfBytes() -> [HalfByte] { + var result = [HalfByte]() + forEach { (byte: UInt8) in + result.append(contentsOf: byte.halfBytes()) + } + return result + } + + init(_ halves: [HalfByte]) { + var halves = halves + var result = [UInt8]() + while halves.count > 1 { + result.append(halves.takeUInt8()) + } + self.init(result) + } +} + +private extension UInt8 { + func halfBytes() -> [HalfByte] { + [HalfByte(value: self >> 4), HalfByte(value: self & 0x0F)] + } + + init(_ left: HalfByte, _ right: HalfByte) { + self.init((left.value << 4) + (right.value & 0x0F)) + } +} + +private extension UInt16 { + init(_ first: HalfByte, _ second: HalfByte, _ third: HalfByte, _ fourth: HalfByte) { + let first = UInt16(first.value) << 12 + let second = UInt16(second.value) << 8 + let third = UInt16(third.value) << 4 + let fourth = UInt16(fourth.value) & 0x0F + let result = first + second + third + fourth + self.init(result) + } +} diff --git a/ElementX/Sources/Other/Extensions/Data.swift b/ElementX/Sources/Other/Extensions/Data.swift new file mode 100644 index 0000000000..4a99299c8b --- /dev/null +++ b/ElementX/Sources/Other/Extensions/Data.swift @@ -0,0 +1,42 @@ +// +// Copyright 2024 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 + +extension Data { + init?(hexString: String) { + self.init() + + var hex = hexString + + // If the hex string has an odd length, prepend a zero + if hex.count % 2 != 0 { + hex = "0" + hex + } + + for i in stride(from: 0, to: hex.count, by: 2) { + let startIndex = hex.index(hex.startIndex, offsetBy: i) + let endIndex = hex.index(hex.startIndex, offsetBy: i + 2) + let byteString = String(hex[startIndex.. @@ -28,12 +27,15 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } + + private var scanTask: Task? init(qrCodeLoginService: QRCodeLoginServiceProtocol, appMediator: AppMediatorProtocol) { self.qrCodeLoginService = qrCodeLoginService self.appMediator = appMediator super.init(initialViewState: QRCodeLoginScreenViewState()) + setupSubscriptions() } // MARK: - Public @@ -46,16 +48,109 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr Task { await startScanIfPossible() } case .openSettings: appMediator.openAppSettings() + case .signInManually: + actionsSubject.send(.signInManually) } } + // MARK: - Private + + private func setupSubscriptions() { + context.$viewState + // not using compactMap before remove duplicates because if there is an error, and the same code needs to be rescanned the transition to nil to clean the state would get ignored. + .map(\.bindings.qrResult) + .removeDuplicates() + .compactMap { $0 } + // this needs to be received on the main actor or the state change for connecting won't work properly + .receive(on: DispatchQueue.main) + .sink { [weak self] qrData in + self?.handleScan(qrData: qrData) + } + .store(in: &cancellables) + + qrCodeLoginService.qrLoginProgressPublisher + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] progress in + MXLog.info("QR Login Progress changed to: \(progress)") + + guard let self, + // Let's not advance the state if the current state is already invalid + !state.state.isError else { + return + } + + switch progress { + case .establishingSecureChannel(_, let stringCode): + state.state = .displayCode(.deviceCode(stringCode)) + case .waitingForToken(let code): + state.state = .displayCode(.verificationCode(code)) + default: + break + } + } + .store(in: &cancellables) + } + private func startScanIfPossible() async { - state.state = await qrCodeLoginService.requestAuthorizationIfNeeded() ? .scan(.scanning) : .error(.noCameraPermission) + state.bindings.qrResult = nil + state.state = await appMediator.requestAuthorizationIfNeeded() ? .scan(.scanning) : .error(.noCameraPermission) } + private func handleScan(qrData: Data) { + guard scanTask == nil else { + return + } + + state.state = .scan(.connecting) + + scanTask = Task { [weak self] in + guard let self else { + return + } + + defer { + scanTask = nil + } + + MXLog.info("Scanning QR code: \(qrData)") + switch await qrCodeLoginService.loginWithQRCode(data: qrData) { + case let .success(session): + MXLog.info("QR Login completed") + actionsSubject.send(.done(userSession: session)) + case .failure(let error): + handleError(error: error) + } + } + } + + private func handleError(error: QRCodeLoginServiceError) { + MXLog.error("Failed to scan the QR code: \(error)") + switch error { + case .invalidQRCode: + state.state = .scan(.invalid) + case .deviceNotSignedIn: + state.state = .scan(.deviceNotSignedIn) + case .cancelled: + state.state = .error(.cancelled) + case .connectionInsecure: + state.state = .error(.connectionNotSecure) + case .declined: + state.state = .error(.declined) + case .linkingNotSupported: + state.state = .error(.linkingNotSupported) + case .expired: + state.state = .error(.expired) + case .deviceNotSupported: + state.state = .error(.deviceNotSupported) + case .failedLoggingIn, .unknown: + state.state = .error(.unknown) + } + } + /// Only for mocking initial states fileprivate init(state: QRCodeLoginState) { - qrCodeLoginService = QRCodeLoginServiceMock(configuration: .init()) + qrCodeLoginService = QRCodeLoginServiceMock() appMediator = AppMediatorMock.default super.init(initialViewState: .init(state: state)) } diff --git a/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift b/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift index b76fa382d2..2727e0f585 100644 --- a/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift +++ b/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift @@ -62,7 +62,7 @@ struct QRCodeLoginScreen: View { SFNumberedListView(items: context.viewState.initialStateListItems) } } bottomContent: { - Button(L10n.actionContinue) { + Button(L10n.screenQrCodeLoginInitialStateButtonTitle) { context.send(viewAction: .startScan) } .buttonStyle(.compound(.primary)) @@ -178,8 +178,11 @@ struct QRCodeLoginScreen: View { } .buttonStyle(.compound(.primary)) - VStack(spacing: 0) { - Label(L10n.screenQrCodeLoginInvalidScanStateSubtitle, icon: \.error, iconSize: .medium, relativeTo: .compound.bodyMDSemibold) + VStack(spacing: 4) { + Label(L10n.screenQrCodeLoginInvalidScanStateSubtitle, + icon: \.error, + iconSize: .medium, + relativeTo: .compound.bodyMDSemibold) .labelStyle(.custom(spacing: 10)) .font(.compound.bodyMDSemibold) .foregroundColor(.compound.textCriticalPrimary) @@ -187,6 +190,29 @@ struct QRCodeLoginScreen: View { Text(L10n.screenQrCodeLoginInvalidScanStateDescription) .foregroundColor(.compound.textSecondary) .font(.compound.bodySM) + .multilineTextAlignment(.center) + } + } + case .deviceNotSignedIn: + VStack(spacing: 16) { + Button(L10n.screenQrCodeLoginInvalidScanStateRetryButton) { + context.send(viewAction: .startScan) + } + .buttonStyle(.compound(.primary)) + + VStack(spacing: 4) { + Label(L10n.screenQrCodeLoginDeviceNotSignedInScanStateSubtitle, + icon: \.error, + iconSize: .medium, + relativeTo: .compound.bodyMDSemibold) + .labelStyle(.custom(spacing: 10)) + .font(.compound.bodyMDSemibold) + .foregroundColor(.compound.textCriticalPrimary) + + Text(L10n.screenQrCodeLoginDeviceNotSignedInScanStateDescription) + .foregroundColor(.compound.textSecondary) + .font(.compound.bodySM) + .multilineTextAlignment(.center) } } } @@ -194,7 +220,7 @@ struct QRCodeLoginScreen: View { } private var qrScanner: some View { - QRCodeScannerView() + QRCodeScannerView(result: $context.qrResult, isScanning: context.viewState.state.isScanning) .aspectRatio(1.0, contentMode: .fill) .frame(maxWidth: 312) .readFrame($qrFrame) @@ -207,7 +233,7 @@ struct QRCodeLoginScreen: View { @ToolbarContentBuilder private var toolbar: some ToolbarContent { ToolbarItem(placement: .cancellationAction) { - if !context.viewState.state.isDisplayingCode { + if context.viewState.state.shouldDisplayCancelButton { Button(L10n.actionCancel) { context.send(viewAction: .cancel) } @@ -273,25 +299,65 @@ struct QRCodeLoginScreen: View { SFNumberedListView(items: context.viewState.connectionNotSecureListItems) } } + default: + simpleErrorStack(errorState: errorState) + } + } + + @ViewBuilder + private func simpleErrorStack(errorState: QRCodeLoginState.QRCodeLoginErrorState) -> some View { + let title = switch errorState { + case .cancelled: + L10n.screenQrCodeLoginErrorCancelledTitle + case .declined: + L10n.screenQrCodeLoginErrorDeclinedTitle + case .expired: + L10n.screenQrCodeLoginErrorExpiredTitle + case .linkingNotSupported: + L10n.screenQrCodeLoginErrorLinkingNotSuportedTitle + case .deviceNotSupported: + L10n.screenQrCodeLoginErrorSlidingSyncNotSupportedTitle(InfoPlistReader.main.bundleDisplayName) case .unknown: - VStack(spacing: 16) { - HeroImage(icon: \.error, style: .critical) + L10n.commonSomethingWentWrong + default: + fatalError("This should not be displayed") + } + + let subtitle: String = switch errorState { + case .cancelled: + L10n.screenQrCodeLoginErrorCancelledSubtitle + case .declined: + L10n.screenQrCodeLoginErrorDeclinedSubtitle + case .expired: + L10n.screenQrCodeLoginErrorExpiredSubtitle + case .linkingNotSupported: + L10n.screenQrCodeLoginErrorLinkingNotSuportedSubtitle(InfoPlistReader.main.bundleDisplayName) + case .deviceNotSupported: + L10n.screenQrCodeLoginErrorSlidingSyncNotSupportedSubtitle(InfoPlistReader.main.bundleDisplayName) + case .unknown: + L10n.screenQrCodeLoginUnknownErrorDescription + default: + fatalError("This should not be displayed") + } + + VStack(spacing: 16) { + HeroImage(icon: \.error, style: .critical) + + VStack(spacing: 8) { + Text(title) + .foregroundColor(.compound.textPrimary) + .font(.compound.headingMDBold) + .multilineTextAlignment(.center) - VStack(spacing: 8) { - Text(L10n.commonSomethingWentWrong) - .foregroundColor(.compound.textPrimary) - .font(.compound.headingMDBold) - .multilineTextAlignment(.center) - - Text(L10n.screenQrCodeLoginUnknownErrorDescription) - .foregroundColor(.compound.textSecondary) - .font(.compound.bodyMD) - .multilineTextAlignment(.center) - } + Text(subtitle) + .foregroundColor(.compound.textSecondary) + .font(.compound.bodyMD) + .multilineTextAlignment(.center) } } } + @ViewBuilder private func errorContentFooter(errorState: QRCodeLoginState.QRCodeLoginErrorState) -> some View { switch errorState { case .noCameraPermission: @@ -299,11 +365,30 @@ struct QRCodeLoginScreen: View { context.send(viewAction: .openSettings) } .buttonStyle(.compound(.primary)) - case .connectionNotSecure, .unknown: + case .connectionNotSecure, .unknown, .expired, .declined, .deviceNotSupported: Button(L10n.screenQrCodeLoginStartOverButton) { context.send(viewAction: .startScan) } .buttonStyle(.compound(.primary)) + case .cancelled: + Button(L10n.actionTryAgain) { + context.send(viewAction: .startScan) + } + .buttonStyle(.compound(.primary)) + case .linkingNotSupported: + VStack(spacing: 16) { + Button(L10n.screenOnboardingSignInManually) { + context.send(viewAction: .signInManually) + } + .buttonStyle(.compound(.primary)) + + Button(L10n.actionCancel) { + context.send(viewAction: .cancel) + } + .padding(.vertical, 13) + .frame(maxWidth: .infinity) + .buttonStyle(.compound(.plain)) + } } } } @@ -336,23 +421,39 @@ private struct QRScannerViewOverlay: View { // MARK: - Previews struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview { + // Initial static let initialStateViewModel = QRCodeLoginScreenViewModel.mock(state: .initial) + // Scanning static let scanningStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.scanning)) static let connectingStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.connecting)) static let invalidStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.invalid)) + static let deviceNotSignedInStateViewModel = QRCodeLoginScreenViewModel.mock(state: .scan(.deviceNotSignedIn)) + + // Display Code + static let deviceCodeStateViewModel = QRCodeLoginScreenViewModel.mock(state: .displayCode(.deviceCode("12"))) + + static let verificationCodeStateViewModel = QRCodeLoginScreenViewModel.mock(state: .displayCode(.verificationCode("123456"))) + + // Errors static let noCameraPermissionStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.noCameraPermission)) static let connectionNotSecureStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.connectionNotSecure)) - static let unknownErrorStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.unknown)) + static let linkingUnsupportedStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.linkingNotSupported)) - static let deviceCodeStateViewModel = QRCodeLoginScreenViewModel.mock(state: .displayCode(.deviceCode("12"))) + static let cancelledStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.cancelled)) - static let verificationCodeStateViewModel = QRCodeLoginScreenViewModel.mock(state: .displayCode(.verificationCode("123456"))) + static let declinedStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.declined)) + + static let expiredStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.expired)) + + static let deviceNoSupportedViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.deviceNotSupported)) + + static let unknownErrorStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.unknown)) static var previews: some View { QRCodeLoginScreen(context: initialStateViewModel.context) @@ -367,19 +468,37 @@ struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview { QRCodeLoginScreen(context: invalidStateViewModel.context) .previewDisplayName("Invalid") + QRCodeLoginScreen(context: deviceNotSignedInStateViewModel.context) + .previewDisplayName("Device not signed in") + + QRCodeLoginScreen(context: deviceCodeStateViewModel.context) + .previewDisplayName("Device code") + + QRCodeLoginScreen(context: verificationCodeStateViewModel.context) + .previewDisplayName("Verification code") + QRCodeLoginScreen(context: noCameraPermissionStateViewModel.context) .previewDisplayName("No Camera Permission") QRCodeLoginScreen(context: connectionNotSecureStateViewModel.context) .previewDisplayName("Connection not secure") - QRCodeLoginScreen(context: unknownErrorStateViewModel.context) - .previewDisplayName("Unknown error") + QRCodeLoginScreen(context: linkingUnsupportedStateViewModel.context) + .previewDisplayName("Linking unsupported") - QRCodeLoginScreen(context: deviceCodeStateViewModel.context) - .previewDisplayName("Device code") + QRCodeLoginScreen(context: cancelledStateViewModel.context) + .previewDisplayName("Cancelled") - QRCodeLoginScreen(context: verificationCodeStateViewModel.context) - .previewDisplayName("Verification code") + QRCodeLoginScreen(context: declinedStateViewModel.context) + .previewDisplayName("Declined") + + QRCodeLoginScreen(context: expiredStateViewModel.context) + .previewDisplayName("Expired") + + QRCodeLoginScreen(context: deviceNoSupportedViewModel.context) + .previewDisplayName("Device not supported") + + QRCodeLoginScreen(context: unknownErrorStateViewModel.context) + .previewDisplayName("Unknown error") } } diff --git a/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeScannerView.swift b/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeScannerView.swift index d5ec974d28..fd11134326 100644 --- a/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeScannerView.swift +++ b/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeScannerView.swift @@ -19,13 +19,48 @@ import SwiftUI import UIKit struct QRCodeScannerView: UIViewControllerRepresentable { + @Binding var result: Data? + var isScanning: Bool + func makeUIViewController(context: Context) -> QRScannerController { let controller = QRScannerController() - + controller.delegate = context.coordinator return controller } - func updateUIViewController(_ uiViewController: QRScannerController, context: Context) { } + func updateUIViewController(_ uiViewController: QRScannerController, context: Context) { + if isScanning { + uiViewController.startScan() + } else { + uiViewController.stopScan() + } + } + + func makeCoordinator() -> Coordinator { + Coordinator($result) + } + + final class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { + @Binding var scanResult: Data? + + init(_ scanResult: Binding) { + _scanResult = scanResult + } + + func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + // Check if the metadataObjects array is not nil and it contains at least one object. + guard metadataObjects.count > 0, + let metadataObj = metadataObjects[0] as? AVMetadataMachineReadableCodeObject, + metadataObj.type == AVMetadataObject.ObjectType.qr, + let data = metadataObj.binaryValue else { + MXLog.info("QRCodeScannerView: invalid qr scan") + return + } + + scanResult = data + MXLog.info("QRCodeScannerView: scanned data") + } + } } final class QRScannerController: UIViewController { @@ -33,8 +68,8 @@ final class QRScannerController: UIViewController { private var videoPreviewLayer: AVCaptureVideoPreviewLayer? private var qrCodeFrameView: UIView? - var delegate: AVCaptureMetadataOutputObjectsDelegate? - + weak var delegate: AVCaptureMetadataOutputObjectsDelegate? + override func viewDidLoad() { super.viewDidLoad() @@ -73,15 +108,26 @@ final class QRScannerController: UIViewController { videoPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill videoPreviewLayer?.frame = view.layer.bounds view.layer.addSublayer(previewLayer) - + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + videoPreviewLayer?.frame = view.layer.bounds + } + + func startScan() { // Start video capture. DispatchQueue.global(qos: .userInitiated).async { self.captureSession.startRunning() + MXLog.info("QRCodeScannerView: capture session started") } } - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - videoPreviewLayer?.frame = view.layer.bounds + func stopScan() { + // Stop video capture. + DispatchQueue.global(qos: .userInitiated).async { + self.captureSession.stopRunning() + MXLog.info("QRCodeScannerView: capture session stopped") + } } } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index b3f8073117..085b8845a4 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -39,12 +39,6 @@ struct DeveloperOptionsScreen: View { } } - Section("QR Code") { - Toggle(isOn: $context.qrCodeLoginEnabled) { - Text("QR code login") - } - } - Section("Room") { Toggle(isOn: $context.shouldCollapseRoomStateEvents) { Text("Collapse room state events") diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift index 37b38752a4..382971495b 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift @@ -32,22 +32,13 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { homeserverSubject = .init(LoginHomeserver(address: appSettings.defaultHomeserverAddress, loginMode: .unknown)) - - let oidcConfiguration = OidcConfiguration(clientName: InfoPlistReader.main.bundleDisplayName, - redirectUri: appSettings.oidcRedirectURL.absoluteString, - clientUri: appSettings.websiteURL.absoluteString, - logoUri: appSettings.logoURL.absoluteString, - tosUri: appSettings.acceptableUseURL.absoluteString, - policyUri: appSettings.privacyURL.absoluteString, - contacts: [appSettings.supportEmailAddress], - staticRegistrations: appSettings.oidcStaticRegistrations.mapKeys { $0.absoluteString }) authenticationService = AuthenticationService(basePath: userSessionStore.baseDirectory.path, passphrase: passphrase, userAgent: UserAgentBuilder.makeASCIIUserAgent(), additionalRootCertificates: [], proxy: appSettings.websiteURL.globalProxy, - oidcConfiguration: oidcConfiguration, + oidcConfiguration: appSettings.oidcConfiguration.rustValue, customSlidingSyncProxy: appSettings.slidingSyncProxyURL?.absoluteString, sessionDelegate: userSessionStore.clientSessionDelegate, crossProcessRefreshLockId: InfoPlistReader.main.bundleIdentifier) diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift index a0b054a69c..81c1e5d943 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift @@ -42,7 +42,7 @@ enum AuthenticationServiceError: Error { protocol AuthenticationServiceProxyProtocol { /// The currently configured homeserver. var homeserver: CurrentValuePublisher { get } - + /// Sets up the service for login on the specified homeserver address. func configure(for homeserverAddress: String) async -> Result /// Performs login using OIDC for the current homeserver. diff --git a/ElementX/Sources/Services/Authentication/OIDCConfigurationProxy.swift b/ElementX/Sources/Services/Authentication/OIDCConfigurationProxy.swift new file mode 100644 index 0000000000..af50e082d4 --- /dev/null +++ b/ElementX/Sources/Services/Authentication/OIDCConfigurationProxy.swift @@ -0,0 +1,45 @@ +// +// Copyright 2024 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 + +struct OIDCConfigurationProxy { + let clientName: String + let redirectURI: URL + let clientURI: URL + let logoURI: URL + let tosURI: URL + let policyURI: URL + let contacts: [String] + let staticRegistrations: [String: String] +} + +#if canImport(MatrixRustSDK) +import MatrixRustSDK + +extension OIDCConfigurationProxy { + var rustValue: OidcConfiguration { + OidcConfiguration(clientName: clientName, + redirectUri: redirectURI.absoluteString, + clientUri: clientURI.absoluteString, + logoUri: logoURI.absoluteString, + tosUri: tosURI.absoluteString, + policyUri: policyURI.absoluteString, + contacts: contacts, + staticRegistrations: staticRegistrations) + } +} +#endif diff --git a/ElementX/Sources/Services/QRCode/QRCodeLoginController.swift b/ElementX/Sources/Services/QRCode/QRCodeLoginController.swift deleted file mode 100644 index f2fc28df3d..0000000000 --- a/ElementX/Sources/Services/QRCode/QRCodeLoginController.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// Copyright 2024 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 AVFoundation - -final class QRCodeLoginService: QRCodeLoginServiceProtocol { - func requestAuthorizationIfNeeded() async -> Bool { - let status = AVCaptureDevice.authorizationStatus(for: .video) - - // Determine if the user previously authorized camera access. - if status == .authorized { - return true - } - - var isAuthorized = false - // If the system hasn't determined the user's authorization status, - // explicitly prompt them for approval. - if status == .notDetermined { - isAuthorized = await AVCaptureDevice.requestAccess(for: .video) - } - - return isAuthorized - } -} diff --git a/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift b/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift new file mode 100644 index 0000000000..7a69fec953 --- /dev/null +++ b/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift @@ -0,0 +1,118 @@ +// +// Copyright 2024 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 Foundation + +import MatrixRustSDK + +final class QRCodeLoginService: QRCodeLoginServiceProtocol { + private let oidcConfiguration: OidcConfiguration + private let passphrase: String + private let userSessionStore: UserSessionStoreProtocol + + private let qrLoginProgressSubject = PassthroughSubject() + var qrLoginProgressPublisher: AnyPublisher { + qrLoginProgressSubject.eraseToAnyPublisher() + } + + init(oidcConfiguration: OidcConfiguration, + encryptionKeyProvider: EncryptionKeyProviderProtocol, + userSessionStore: UserSessionStoreProtocol) { + self.oidcConfiguration = oidcConfiguration + self.userSessionStore = userSessionStore + passphrase = encryptionKeyProvider.generateKey().base64EncodedString() + } + + func loginWithQRCode(data: Data) async -> Result { + let qrData: QrCodeData + do { + qrData = try QrCodeData.fromBytes(bytes: data) + } catch { + MXLog.error("QRCode decode error: \(error)") + return .failure(.invalidQRCode) + } + + let listener = QrLoginProgressListenerProxy { [weak self] progress in + self?.qrLoginProgressSubject.send(progress) + } + + do { + let client = try await ClientBuilder() + .basePath(path: userSessionStore.baseDirectory.path(percentEncoded: false)) + .passphrase(passphrase: passphrase) + .userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent()) + .enableCrossProcessRefreshLock(processId: InfoPlistReader.main.bundleIdentifier, + sessionDelegate: userSessionStore.clientSessionDelegate) + .serverVersions(versions: ["v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"]) // FIXME: Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376 + .buildWithQrCode(qrCodeData: qrData, oidcConfiguration: oidcConfiguration, progressListener: listener) + return await login(client: client) + } catch let error as HumanQrLoginError { + MXLog.error("QRCode login error: \(error)") + return .failure(error.serviceError) + } catch { + MXLog.error("QRCode login unknown error: \(error)") + return .failure(.unknown) + } + } + + private func login(client: Client) async -> Result { + switch await userSessionStore.userSession(for: client, passphrase: passphrase) { + case .success(let session): + return .success(session) + case .failure(let error): + MXLog.error("QRCode login failed error: \(error)") + return .failure(.failedLoggingIn) + } + } +} + +final class QrLoginProgressListenerProxy: QrLoginProgressListener { + private let onUpdateClosure: (QrLoginProgress) -> Void + + init(onUpdateClosure: @escaping (QrLoginProgress) -> Void) { + self.onUpdateClosure = onUpdateClosure + } + + func onUpdate(state: QrLoginProgress) { + onUpdateClosure(state) + } +} + +private extension HumanQrLoginError { + var serviceError: QRCodeLoginServiceError { + switch self { + case .Cancelled: + return .cancelled + case .ConnectionInsecure: + return .connectionInsecure + case .Declined: + return .declined + case .LinkingNotSupported: + return .linkingNotSupported + case .Expired: + return .expired + case .InvalidQrCode: + return .invalidQRCode + case .SlidingSyncNotAvailable: + return .deviceNotSupported + case .OtherDeviceNotSignedIn: + return .deviceNotSignedIn + case .Unknown, .OidcMetadataInvalid: + return .unknown + } + } +} diff --git a/ElementX/Sources/Services/QRCode/QRCodeLoginServiceProtocol.swift b/ElementX/Sources/Services/QRCode/QRCodeLoginServiceProtocol.swift index 2a73242436..bc0dca2254 100644 --- a/ElementX/Sources/Services/QRCode/QRCodeLoginServiceProtocol.swift +++ b/ElementX/Sources/Services/QRCode/QRCodeLoginServiceProtocol.swift @@ -14,7 +14,27 @@ // limitations under the License. // +import Combine +import Foundation + +import MatrixRustSDK + +enum QRCodeLoginServiceError: Error { + case failedLoggingIn + case invalidQRCode + case cancelled + case connectionInsecure + case declined + case linkingNotSupported + case expired + case deviceNotSupported + case deviceNotSignedIn + case unknown +} + // sourcery: AutoMockable protocol QRCodeLoginServiceProtocol { - func requestAuthorizationIfNeeded() async -> Bool + var qrLoginProgressPublisher: AnyPublisher { get } + + func loginWithQRCode(data: Data) async -> Result } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 6492d7b144..516179136e 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -130,6 +130,7 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .authenticationFlow: let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: MockAuthenticationServiceProxy(), + qrCodeLoginService: QRCodeLoginServiceMock(), bugReportService: BugReportServiceMock(), navigationRootCoordinator: navigationRootCoordinator, appMediator: AppMediatorMock.default, diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Cancelled.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Cancelled.png new file mode 100644 index 0000000000..8104107a5d --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Cancelled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06198ff9cd5220d7c1282079371728698a9b9ac6d8058a4e09e90c4284b15658 +size 118241 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Connection-not-secure.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Connection-not-secure.png index 7096bd8191..20762cd0dd 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Connection-not-secure.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Connection-not-secure.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81808a0d38699a96abea059176bf065956bbbe2f457cb58bd4f8ec008e043807 -size 240316 +oid sha256:fb5249849da514c75ada263c8801339d7d23fe35d9e5e4096acb35a55b69b458 +size 233785 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Declined.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Declined.png new file mode 100644 index 0000000000..1db1683542 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Declined.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8291c2203da7953c7b72e46b28ea7f3de99475fa5b89316cbdf1ddb14735f07 +size 118456 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Device-not-signed-in.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Device-not-signed-in.png new file mode 100644 index 0000000000..661dd69dd1 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Device-not-signed-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32854697cdb9c9f4e84282193921cc0a2c959f34ab1bf6be2182ebaf06178935 +size 156558 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Device-not-supported.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Device-not-supported.png new file mode 100644 index 0000000000..8929a82495 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Device-not-supported.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:898bc820b2ddcf43903b67790fb4f33fd4cfe525360b3fd6daea043b0fa4bba3 +size 121939 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Expired.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Expired.png new file mode 100644 index 0000000000..22099ddd29 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Expired.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5aafdb89352ede1e946d9462cad3885be7bcd87038e7194420a31549488cd45a +size 122996 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Initial.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Initial.png index f1df1983f9..e01454406d 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Initial.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:608c132245379fca44da426c23d05e4322d0b5205266c27e8c7f2d702c10c6ab -size 175909 +oid sha256:e762f2d946c682c59b7b6c1aaa56e5596bb64de67ec29eca6ef8b1ac92ad8d8c +size 178544 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Invalid.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Invalid.png index f815e01def..c0beee0a6e 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Invalid.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Invalid.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:137d7d05b66b23ddb027f2d44fa31f3dadd35d5e1668f570c648833ea016d485 -size 139013 +oid sha256:64c7afdfb4698c972203b5987fcb9036b4aa75645969c22eefc54566a4e65747 +size 139192 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Linking-unsupported.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Linking-unsupported.png new file mode 100644 index 0000000000..b8ed1dbda0 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Linking-unsupported.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16c2e970674c36f0e40b52d1005d3e45dbf5f29123cb866aea643d8d5ea8b976 +size 153497 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Unknown-error.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Unknown-error.png index b40b1b6fcf..1814882f15 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Unknown-error.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-en-GB.Unknown-error.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19143f0cb869a917d7bff9d6c510ad5c589dad58f2f6a1f96c43402c8cb7eaca -size 130219 +oid sha256:99707d136259b3fab5664ad24ca5923554f92d94c9e913be9d9b776e942b464d +size 124228 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Cancelled.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Cancelled.png new file mode 100644 index 0000000000..9ea56f8a7e --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Cancelled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c59ce9bc5b29e9b36d53f6dec406b4514f4d51cd65d34f544987078555936018 +size 152784 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Connection-not-secure.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Connection-not-secure.png index 258c2fa522..9ef710fa2b 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Connection-not-secure.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Connection-not-secure.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e532918bbc3dd6f19b1c6f595bf4e78328777f67ba168ca73820acf36c4b5fb3 -size 364040 +oid sha256:9f881dfeb084cbd409e9fdf7913252c6d1b841e2f875a86916d7c69dde989970 +size 353783 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Declined.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Declined.png new file mode 100644 index 0000000000..41aba0b4a4 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Declined.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cbe50417a3b6985b69a8465242d65af404dfccd919fc971aea864daaf4b6b47 +size 150711 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Device-not-signed-in.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Device-not-signed-in.png new file mode 100644 index 0000000000..a2ad95a966 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Device-not-signed-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61c704f3d7b69ea192624ec6fb60fca7dfe6e089be632d4e22e72a3d1e13cdc6 +size 208122 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Device-not-supported.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Device-not-supported.png new file mode 100644 index 0000000000..3f774c629e --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Device-not-supported.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb425e78ece3246a1b290c8b84d1caa04d4246c86bf20eaa0b74c774eb618327 +size 151075 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Expired.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Expired.png new file mode 100644 index 0000000000..a1fc08bd48 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Expired.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5af5a51a6ae879671bfdf417d9f9cc7715e20512d85379373f9e8104b4c2ee4c +size 163873 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Initial.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Initial.png index 6ee423fc1c..94e18caebd 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Initial.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b26435b6414ee0c2782fe084939075016fd26533fc9af16ed7645dd2f3a1347 -size 256639 +oid sha256:c1cd2940d92227d7036640f6ea64eddd4f6245ab04662eee67b50e1162e09a9d +size 262484 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Invalid.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Invalid.png index 44ab04228b..9d061d6ce1 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Invalid.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Invalid.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82fa86b47675cb62a236b024948037afca1cde901a22605791d2a91cd3c79b56 -size 174326 +oid sha256:446735c3777cd614aa008beae3b0e3fc744e6c4cc44ac7f78dd082a9b02d7625 +size 174596 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Linking-unsupported.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Linking-unsupported.png new file mode 100644 index 0000000000..fb434e980f --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Linking-unsupported.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59fda8126fd1881e419aea528308604efadaefa1b18679e5f263ba3f84f14d2e +size 220251 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Unknown-error.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Unknown-error.png index 39877da5a9..45ded4e397 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Unknown-error.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPad-pseudo.Unknown-error.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2fc9f0ebae6cf4c1521ffbde06fbdad3bc15071db2a9461abf1b7d1415f113e -size 164912 +oid sha256:598203104fef16dfa93e6d77b1529892774c0be30f95344ff800b7c64698b6ac +size 153117 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Cancelled.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Cancelled.png new file mode 100644 index 0000000000..15dc9fc8d6 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Cancelled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1428741d616b4cdb28973473c2602392a9c2ca0ef414e90fa40d110a1ed29ef6 +size 71029 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Connection-not-secure.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Connection-not-secure.png index 92f18204a1..1e31ec9280 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Connection-not-secure.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Connection-not-secure.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d2b9794ad3b7d038113d0b87bd0f694194471adb29357e963f8143d955dc4ac -size 164424 +oid sha256:eca687f56b650ef5ac41219061a45dac5b56d9eaff54da40e869162ced207d08 +size 159528 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Declined.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Declined.png new file mode 100644 index 0000000000..b6b9411763 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Declined.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1ab0cb4abb4cad384c95d5b788434a8e6a3449fac4e6bd0d73a6cfc8b0bf55f +size 68137 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Device-not-signed-in.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Device-not-signed-in.png new file mode 100644 index 0000000000..1a8aef9250 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Device-not-signed-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b42499314b0f777f32878783bb6ba28009da920c74a08b69e21f55d1677085c +size 94623 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Device-not-supported.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Device-not-supported.png new file mode 100644 index 0000000000..1d3b27f7ac --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Device-not-supported.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53f9ce852ff302cc2b9cc52d7d057becda1ae34eefc4cc8cfd98631e9d249c6a +size 72003 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Expired.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Expired.png new file mode 100644 index 0000000000..4c6e327a39 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Expired.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d531f55191cb88f6678f6950a26d9f3eb382d95c41046c49d50a8c78724b9f1 +size 72077 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Initial.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Initial.png index a05e87330e..27f37caa19 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Initial.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e19a0e1e323537e6a70da304e0be9bbb11643371793514597e022e0301732ca -size 124161 +oid sha256:21d7f98e4f6c9b25aa5a812a5410b6fe2fb4f202b1e556514554ff6afe7e200a +size 126332 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Invalid.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Invalid.png index 7fce2a8015..e35c693abf 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Invalid.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Invalid.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed2670440f91d41f983081d877f7c084ca205724143f5b5d37dc6fdb52fc188c -size 78633 +oid sha256:8f4efd9d313010c3961beb4ad8f85c7179cfe17c0900bf6a3150ca1070670f0f +size 78648 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Linking-unsupported.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Linking-unsupported.png new file mode 100644 index 0000000000..b1d9117007 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Linking-unsupported.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60d9429395be091a64a79de2bd8d03152777bbf9ac5dd353e1f0fe4a1406c04f +size 101877 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Unknown-error.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Unknown-error.png index 99997a1abf..ae3425e8d1 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Unknown-error.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-en-GB.Unknown-error.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a70dec75d62f4ebb3d00f613c3314c741bef3cc757d5c9b78358ca8b9535d52 -size 78473 +oid sha256:6dabb5a3e2d2f084d783367321ae139da12a3d59cc2cd908a11fe5c27cc57e21 +size 73675 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Cancelled.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Cancelled.png new file mode 100644 index 0000000000..36d74e20ff --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Cancelled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:986eabb153ea07fd2caa7239fad835b13363674b67e3d40f743496eeea6c83ba +size 97338 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Connection-not-secure.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Connection-not-secure.png index ab8b67cdd3..5ebb7cd0d8 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Connection-not-secure.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Connection-not-secure.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54b0bc2bacf2504febf374970d5afdbf8f8f03b139005c5c81d04761faf66132 -size 268685 +oid sha256:b02fa3348fb1bc56e1bd514322d157fae8247228239d20a369a178fe10f2eea8 +size 260577 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Declined.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Declined.png new file mode 100644 index 0000000000..e580a09a3f --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Declined.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73ffa2cb86e0228ea0788e53af38e3d3fc2aa1e14d5b3d1358dfc4bc3f5f474b +size 95518 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Device-not-signed-in.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Device-not-signed-in.png new file mode 100644 index 0000000000..2d84c1a143 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Device-not-signed-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be6ad96a182495c28b889d2c1a6d2456f1cf27b2f443cf0ee6a93d079bad80be +size 141938 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Device-not-supported.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Device-not-supported.png new file mode 100644 index 0000000000..7cdc9e0657 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Device-not-supported.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a463d00012f4f3ea2bf35e87c6b35e425ba9865ae77b2852735b1f4c7c02627d +size 97564 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Expired.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Expired.png new file mode 100644 index 0000000000..0ea9191bdf --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Expired.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db8e449dc3993188507e23f79de263ed15a9e75188d1e70ecc5370a444bbdebd +size 103468 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Initial.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Initial.png index f355079f65..03b3e5cb15 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Initial.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c1f4e2f5924db1858c2417235b24e3333c3570622b02a85ec1e54e1bcf9504c -size 194638 +oid sha256:a412452ca11c6e7c53d7ad4e01faa8c4bc3979771912c2b96b13422cef30d9bd +size 199595 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Invalid.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Invalid.png index bbb460d805..fe7a060920 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Invalid.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Invalid.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1f5a01424b85977592a295500142b1ebf3c6d0720c45b03334f108f1bc03f1e -size 114199 +oid sha256:42f429a79217b6da6f567ad9e9c7cc365e65fe005ad4773bbf370998420f9e80 +size 114340 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Linking-unsupported.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Linking-unsupported.png new file mode 100644 index 0000000000..4f730fe654 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Linking-unsupported.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b479b466cc5b987468386a09286428f4105ade82f27296c194d4950bf7f28bf +size 157737 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Unknown-error.png b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Unknown-error.png index f8d1c750a5..c3c766b252 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Unknown-error.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_qRCodeLoginScreen-iPhone-15-pseudo.Unknown-error.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d7c1eb55621a0c337ecc6c7882fdd6c48d55ca6ba9bceaaa7e334dc123b0b1e -size 111028 +oid sha256:9743e035cb1d249ce7d4d9f7602d01d35d2887eb614a7cffb34d8abc357144ef +size 102855 diff --git a/UnitTests/Sources/AVMetadataMachineReadableCodeObjectExtensionsTest.swift b/UnitTests/Sources/AVMetadataMachineReadableCodeObjectExtensionsTest.swift new file mode 100644 index 0000000000..e08c2fde42 --- /dev/null +++ b/UnitTests/Sources/AVMetadataMachineReadableCodeObjectExtensionsTest.swift @@ -0,0 +1,49 @@ +// +// Copyright 2024 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 AVKit +import XCTest + +@testable import ElementX + +final class AVMetadataMachineReadableCodeObjectExtensionsTest: XCTestCase { + func testDecoQRCodeVersion8() { + // swiftlint:disable:next line_length + let rawDataHexString = "4a34d415452495802048bf94b094096e57d3ea43545604cf59b1704879d295cf7fdd99c62df7866da36005668747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f5f73796e617073652f636c69656e742f72656e64657a766f75732f3031485a32394d345936374a4e315658505759464e355a363638002168747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f0ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec" + // swiftlint:disable:next line_length + let expectedDecodedString = "4d415452495802048bf94b094096e57d3ea43545604cf59b1704879d295cf7fdd99c62df7866da36005668747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f5f73796e617073652f636c69656e742f72656e64657a766f75732f3031485a32394d345936374a4e315658505759464e355a363638002168747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f" + let symbolVersion = 8 + + guard let data = Data(hexString: rawDataHexString) else { + XCTFail("Could not initialise the raw data") + return + } + + guard let resultData = AVMetadataMachineReadableCodeObject.removeQrProtocolData(data, symbolVersion: symbolVersion) else { + XCTFail("Could not remove the protocol data") + return + } + + let resultString = resultData.map { String(format: "%02x", $0) }.joined() + XCTAssertEqual(expectedDecodedString, resultString) + + guard let expectedResultData = Data(hexString: expectedDecodedString) else { + XCTFail("Could not initialise the decoded data") + return + } + XCTAssertEqual(expectedResultData, resultData) + } +} diff --git a/UnitTests/Sources/QRCodeLoginScreenViewModelTests.swift b/UnitTests/Sources/QRCodeLoginScreenViewModelTests.swift index 49bcadef36..dd444b86b4 100644 --- a/UnitTests/Sources/QRCodeLoginScreenViewModelTests.swift +++ b/UnitTests/Sources/QRCodeLoginScreenViewModelTests.swift @@ -14,9 +14,104 @@ // limitations under the License. // +import Combine import XCTest +import MatrixRustSDK + @testable import ElementX @MainActor -class QRCodeLoginScreenViewModelTests: XCTestCase { } +final class QRCodeLoginScreenViewModelTests: XCTestCase { + private var qrProgressSubject: PassthroughSubject! + private var qrServiceMock: QRCodeLoginServiceMock! + private var appMediatorMock: AppMediatorMock! + private var viewModel: QRCodeLoginScreenViewModelProtocol! + + private var context: QRCodeLoginScreenViewModelType.Context { + viewModel.context + } + + override func setUp() { + qrProgressSubject = PassthroughSubject() + qrServiceMock = QRCodeLoginServiceMock() + qrServiceMock.underlyingQrLoginProgressPublisher = qrProgressSubject.eraseToAnyPublisher() + appMediatorMock = AppMediatorMock.default + viewModel = QRCodeLoginScreenViewModel(qrCodeLoginService: qrServiceMock, + appMediator: appMediatorMock) + } + + func testInitialState() { + XCTAssertEqual(context.viewState.state, .initial) + XCTAssertNil(context.qrResult) + XCTAssertFalse(qrServiceMock.loginWithQRCodeDataCalled) + XCTAssertFalse(appMediatorMock.requestAuthorizationIfNeededCalled) + XCTAssertFalse(appMediatorMock.openAppSettingsCalled) + } + + func testRequestCameraPermission() async throws { + appMediatorMock.requestAuthorizationIfNeededReturnValue = false + XCTAssert(context.viewState.state == .initial) + + let deferred = deferFulfillment(viewModel.context.$viewState) { state in + state.state == .error(.noCameraPermission) + } + context.send(viewAction: .startScan) + try await deferred.fulfill() + XCTAssertTrue(appMediatorMock.requestAuthorizationIfNeededCalled) + + context.send(viewAction: .openSettings) + await Task.yield() + XCTAssertTrue(appMediatorMock.openAppSettingsCalled) + XCTAssertNil(context.qrResult) + } + + func testLogin() async throws { + var isCompleted = false + qrServiceMock.loginWithQRCodeDataClosure = { _ in + while !isCompleted { + await Task.yield() + } + return .success(UserSessionMock(.init(clientProxy: ClientProxyMock()))) + } + + XCTAssert(context.viewState.state == .initial) + + var deferred = deferFulfillment(context.$viewState) { state in + state.state == .scan(.scanning) + } + context.send(viewAction: .startScan) + try await deferred.fulfill() + XCTAssertTrue(appMediatorMock.requestAuthorizationIfNeededCalled) + + deferred = deferFulfillment(context.$viewState) { state in + state.state == .scan(.connecting) + } + context.qrResult = .init() + try await deferred.fulfill() + + deferred = deferFulfillment(context.$viewState) { state in + state.state == .displayCode(.deviceCode("01")) + } + qrProgressSubject.send(.establishingSecureChannel(checkCode: 1, checkCodeString: "01")) + try await deferred.fulfill() + + deferred = deferFulfillment(context.$viewState) { state in + state.state == .displayCode(.verificationCode("ABCDEF")) + } + qrProgressSubject.send(.waitingForToken(userCode: "ABCDEF")) + try await deferred.fulfill() + + let deferredAction = deferFulfillment(viewModel.actionsPublisher) { action in + switch action { + case .done: + return true + default: + return false + } + } + qrProgressSubject.send(.done) + isCompleted = true + try await deferredAction.fulfill() + } +} diff --git a/project.yml b/project.yml index 62414cbffa..66fc1dd8c8 100644 --- a/project.yml +++ b/project.yml @@ -49,7 +49,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/element-hq/matrix-rust-components-swift - exactVersion: 1.0.4 + exactVersion: 1.0.6 # path: ../matrix-rust-sdk Compound: url: https://github.com/element-hq/compound-ios