QR Code Login Flow (#2767)

Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
Co-authored-by: Hugh Nimmo-Smith <hughns@element.io>
This commit is contained in:
Mauro
2024-05-29 17:07:17 +02:00
committed by GitHub
parent 66a8f6f3d1
commit 4e23f7be22
72 changed files with 1241 additions and 226 deletions
+36 -32
View File
@@ -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 = "<group>"; };
035177BCD8E8308B098AC3C2 /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = "<group>"; };
0376C429FAB1687C3D905F3E /* MockCoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCoder.swift; sourceTree = "<group>"; };
0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = "<group>"; };
0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_voice_message.m4a; sourceTree = "<group>"; };
03DD998E523D4EC93C7ED703 /* RoomNotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelTests.swift; sourceTree = "<group>"; };
044E501B8331B339874D1B96 /* CompoundIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompoundIcon.swift; sourceTree = "<group>"; };
045253F9967A535EE5B16691 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = "<group>"; };
048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityIdentifiers.swift; sourceTree = "<group>"; };
04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxyProtocol.swift; sourceTree = "<group>"; };
@@ -1200,7 +1203,7 @@
127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = "<group>"; };
128501375217576AF0FE3E92 /* RoomAttachmentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAttachmentPicker.swift; sourceTree = "<group>"; };
12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.swift; sourceTree = "<group>"; };
1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = "<group>"; };
1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = "<group>"; };
130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = "<group>"; };
136F80A613B55BDD071DCEA5 /* JoinRoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenModels.swift; sourceTree = "<group>"; };
13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -1288,7 +1291,7 @@
25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = "<group>"; };
25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenCoordinator.swift; sourceTree = "<group>"; };
260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = "<group>"; };
267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; path = PreviewTests.xctestplan; sourceTree = "<group>"; };
267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PreviewTests.xctestplan; sourceTree = "<group>"; };
26B0A96B8FE4849227945067 /* VoiceMessageRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorder.swift; sourceTree = "<group>"; };
26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceProtocol.swift; sourceTree = "<group>"; };
2721D7B051F0159AA919DA05 /* RoomChangePermissionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@@ -1333,6 +1336,7 @@
32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyProtocol.swift; sourceTree = "<group>"; };
32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelTests.swift; sourceTree = "<group>"; };
330AF4D121C3396F7A14B21D /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/SAS.strings; sourceTree = "<group>"; };
3339B1DDB1341E833D2555BC /* AVMetadataMachineReadableCodeObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVMetadataMachineReadableCodeObject.swift; sourceTree = "<group>"; };
3368395F06AA180138E185B6 /* PollFormScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenUITests.swift; sourceTree = "<group>"; };
33AE897D86784CCA5E4E9227 /* ElementCallService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallService.swift; sourceTree = "<group>"; };
33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -1346,7 +1350,7 @@
3558A15CFB934F9229301527 /* RestorationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationToken.swift; sourceTree = "<group>"; };
35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
35FA991289149D31F4286747 /* UserPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreference.swift; sourceTree = "<group>"; };
36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyMock.swift; sourceTree = "<group>"; };
376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerURLBuildersTests.swift; sourceTree = "<group>"; };
37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItem.swift; sourceTree = "<group>"; };
@@ -1625,7 +1629,6 @@
7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceProtocol.swift; sourceTree = "<group>"; };
7FB2253D36E81E045E1CB432 /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = "<group>"; };
7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenModels.swift; sourceTree = "<group>"; };
80A07343F18BB8EAC17B07B7 /* QRCodeLoginController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginController.swift; sourceTree = "<group>"; };
80C4927D09099497233E9980 /* WaitlistScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreen.swift; sourceTree = "<group>"; };
80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageEventStringBuilder.swift; sourceTree = "<group>"; };
8166F121C79C7B62BF01D508 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pt; path = pt.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -1681,7 +1684,7 @@
8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = "<group>"; };
8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = "<group>"; };
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = "<group>"; };
8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = "<group>"; };
8F6210134203BE1F2DD5C679 /* RoomDirectoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectoryCell.swift; sourceTree = "<group>"; };
8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenViewModelTests.swift; sourceTree = "<group>"; };
@@ -1690,7 +1693,6 @@
90791B9C739C716A40E1B230 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
907FA4DE17DEA1A3738EFB83 /* AudioRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorder.swift; sourceTree = "<group>"; };
90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = "<group>"; };
90B4ED923603F6110D4960C5 /* QRCodeLoginServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginServiceMock.swift; sourceTree = "<group>"; };
90DFF217B3D9D0941283278C /* RoomRolesAndPermissionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
90F2F8998E5632668B0AD848 /* RoomTimelineItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemView.swift; sourceTree = "<group>"; };
913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachmentData.swift; sourceTree = "<group>"; };
@@ -1767,9 +1769,12 @@
A436057DBEA1A23CA8CB1FD7 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = "<group>"; };
A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetectionTests.swift; sourceTree = "<group>"; };
A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; };
A69869844D2B6F5BD9AABF85 /* OIDCConfigurationProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCConfigurationProxy.swift; sourceTree = "<group>"; };
A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = "<group>"; };
A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriverProtocol.swift; sourceTree = "<group>"; };
A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = "<group>"; };
A78DD8682C0766B1005A32A9 /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVMetadataMachineReadableCodeObjectExtensionsTest.swift; sourceTree = "<group>"; };
A78DD86A2C076B3F005A32A9 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = "<group>"; };
A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
A84D413BF49F0E980F010A6B /* LogViewerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenCoordinator.swift; sourceTree = "<group>"; };
@@ -1830,13 +1835,14 @@
B40233F2989AD49906BB310D /* RoomPollsHistoryScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelTests.swift; sourceTree = "<group>"; };
B410B32B72C90BF94E481F33 /* AppLockSetupPINScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenModels.swift; sourceTree = "<group>"; };
B43456E73F8A2D52B69B9FB9 /* TemplateScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModel.swift; sourceTree = "<group>"; };
B4427AF4B7FB7EF3E3D424C7 /* QRCodeLoginService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginService.swift; sourceTree = "<group>"; };
B48B7AD4908C5C374517B892 /* MapAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MapAssets.xcassets; sourceTree = "<group>"; };
B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+SwiftUI.swift"; sourceTree = "<group>"; };
B50F03079F6B5EF9CA005F14 /* TimelineProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProxyProtocol.swift; sourceTree = "<group>"; };
B53AC78E49A297AC1D72A7CF /* AppMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediator.swift; sourceTree = "<group>"; };
B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = "<group>"; };
B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = "<group>"; };
B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = "<group>"; };
B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = "<group>"; };
B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = "<group>"; };
B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = "<group>"; };
B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetails.swift; sourceTree = "<group>"; };
@@ -1947,7 +1953,7 @@
CE47A97726F0675DEE387BF9 /* TypingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = "<group>"; };
CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = "<group>"; };
CEE20623EB4A9B88FB29F2BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SAS.strings; sourceTree = "<group>"; };
CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = "<group>"; };
CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = "<group>"; };
CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = "<group>"; };
D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = "<group>"; };
D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@@ -2070,7 +2076,7 @@
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = "<group>"; };
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModel.swift; sourceTree = "<group>"; };
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; };
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = "<group>"; };
ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = "<group>"; };
ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = "<group>"; };
EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = "<group>"; };
@@ -2093,7 +2099,7 @@
F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = "<group>"; };
F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = "<group>"; };
F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = "<group>"; };
F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = "<group>"; };
F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = "<group>"; };
F2E4EF80DFB8FE7C4469B15D /* RoomDirectorySearchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreen.swift; sourceTree = "<group>"; };
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = "<group>"; };
F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -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 = "<group>";
@@ -4338,6 +4346,7 @@
CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */,
4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */,
65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */,
A69869844D2B6F5BD9AABF85 /* OIDCConfigurationProxy.swift */,
);
path = Authentication;
sourceTree = "<group>";
@@ -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" */ = {
@@ -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"
}
},
{
@@ -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";
@@ -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,
@@ -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
}
}
@@ -33,6 +33,8 @@ protocol AppMediatorProtocol {
func openAppSettings()
func setIdleTimerDisabled(_ disabled: Bool)
func requestAuthorizationIfNeeded() async -> Bool
}
extension UIApplication.State: CustomStringConvertible {
@@ -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
@@ -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<AnyCancellable>()
@@ -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)
@@ -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
+32
View File
@@ -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
+2 -1
View File
@@ -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
@@ -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<QrLoginProgress, Never> {
get { return underlyingQrLoginProgressPublisher }
set(value) { underlyingQrLoginProgressPublisher = value }
}
var underlyingQrLoginProgressPublisher: AnyPublisher<QrLoginProgress, Never>!
//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<UserSessionProtocol, QRCodeLoginServiceError>!
var loginWithQRCodeDataReturnValue: Result<UserSessionProtocol, QRCodeLoginServiceError>! {
get {
if Thread.isMainThread {
return requestAuthorizationIfNeededUnderlyingReturnValue
return loginWithQRCodeDataUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
var returnValue: Result<UserSessionProtocol, QRCodeLoginServiceError>? = 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<UserSessionProtocol, QRCodeLoginServiceError>)?
func requestAuthorizationIfNeeded() async -> Bool {
requestAuthorizationIfNeededCallsCount += 1
if let requestAuthorizationIfNeededClosure = requestAuthorizationIfNeededClosure {
return await requestAuthorizationIfNeededClosure()
func loginWithQRCode(data: Data) async -> Result<UserSessionProtocol, QRCodeLoginServiceError> {
loginWithQRCodeDataCallsCount += 1
loginWithQRCodeDataReceivedData = data
loginWithQRCodeDataReceivedInvocations.append(data)
if let loginWithQRCodeDataClosure = loginWithQRCodeDataClosure {
return await loginWithQRCodeDataClosure(data)
} else {
return requestAuthorizationIfNeededReturnValue
return loginWithQRCodeDataReturnValue
}
}
}
@@ -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
}
}
@@ -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..<charactersCount {
output.append(input.takeUInt8())
}
return output
case 0x00: // End of data
return []
default:
return []
}
}
}
private struct HalfByte {
let value: UInt8
}
private extension [HalfByte] {
mutating func takeUInt8() -> 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)
}
}
@@ -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..<endIndex])
if let byte = UInt8(byteString, radix: 16) {
append(byte)
} else {
return nil
}
}
}
}
@@ -31,9 +31,13 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
self.appSettings = appSettings
super.init(initialViewState: AuthenticationStartScreenViewState())
if !ProcessInfo.processInfo.isiOSAppOnMac {
appSettings.$qrCodeLoginEnabled
.weakAssign(to: \.state.isQRCodeLoginEnabled, on: self)
.store(in: &cancellables)
if appSettings.isDevelopmentBuild {
state.isQRCodeLoginEnabled = true
} else {
appSettings.$qrCodeLoginEnabled
.weakAssign(to: \.state.isQRCodeLoginEnabled, on: self)
.store(in: &cancellables)
}
}
}
@@ -27,6 +27,8 @@ struct QRCodeLoginScreenCoordinatorParameters {
enum QRCodeLoginScreenCoordinatorAction {
case cancel
case signInManually
case done(userSession: UserSessionProtocol)
}
final class QRCodeLoginScreenCoordinator: CoordinatorProtocol {
@@ -52,8 +54,12 @@ final class QRCodeLoginScreenCoordinator: CoordinatorProtocol {
guard let self else { return }
switch action {
case .signInManually:
self.actionsSubject.send(.signInManually)
case .cancel:
self.actionsSubject.send(.cancel)
case .done(let userSession):
self.actionsSubject.send(.done(userSession: userSession))
}
}
.store(in: &cancellables)
@@ -18,6 +18,8 @@ import Foundation
enum QRCodeLoginScreenViewModelAction {
case cancel
case signInManually
case done(userSession: UserSessionProtocol)
}
struct QRCodeLoginScreenViewState: BindableState {
@@ -44,11 +46,18 @@ struct QRCodeLoginScreenViewState: BindableState {
AttributedString(L10n.screenQrCodeLoginConnectionNoteSecureStateListItem2),
AttributedString(L10n.screenQrCodeLoginConnectionNoteSecureStateListItem3)
]
var bindings = QRCodeLoginScreenViewStateBindings()
}
struct QRCodeLoginScreenViewStateBindings {
var qrResult: Data?
}
enum QRCodeLoginScreenViewAction {
case cancel
case startScan
case signInManually
case openSettings
}
@@ -65,6 +74,11 @@ enum QRCodeLoginState: Equatable {
enum QRCodeLoginErrorState: Equatable {
case noCameraPermission
case connectionNotSecure
case cancelled
case declined
case expired
case linkingNotSupported
case deviceNotSupported
case unknown
}
@@ -75,6 +89,8 @@ enum QRCodeLoginState: Equatable {
case connecting
/// the qr code has been processed and is invalid
case invalid
/// the qr code has been processed but it belongs to a device not signed in,
case deviceNotSignedIn
}
enum QRCodeLoginDisplayCodeState: Equatable {
@@ -99,4 +115,42 @@ enum QRCodeLoginState: Equatable {
return false
}
}
var isScanning: Bool {
switch self {
case .scan(let state):
return state == .scanning
default:
return false
}
}
var isError: Bool {
switch self {
case .error:
return true
case let .scan(state):
switch state {
case .invalid, .deviceNotSignedIn:
return true
default:
return false
}
default:
return false
}
}
var shouldDisplayCancelButton: Bool {
switch self {
case .initial:
return true
case .scan:
return true
case .error(let error):
return error == .noCameraPermission
default:
return false
}
}
}
@@ -14,9 +14,8 @@
// limitations under the License.
//
import AVFoundation
import Combine
import SwiftUI
import Foundation
typealias QRCodeLoginScreenViewModelType = StateStoreViewModel<QRCodeLoginScreenViewState, QRCodeLoginScreenViewAction>
@@ -28,12 +27,15 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
var actionsPublisher: AnyPublisher<QRCodeLoginScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
private var scanTask: Task<Void, Never>?
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)
}
}
private func startScanIfPossible() async {
state.state = await qrCodeLoginService.requestAuthorizationIfNeeded() ? .scan(.scanning) : .error(.noCameraPermission)
// 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.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))
}
@@ -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,24 +421,40 @@ 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 linkingUnsupportedStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.linkingNotSupported))
static let cancelledStateViewModel = QRCodeLoginScreenViewModel.mock(state: .error(.cancelled))
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 let deviceCodeStateViewModel = QRCodeLoginScreenViewModel.mock(state: .displayCode(.deviceCode("12")))
static let verificationCodeStateViewModel = QRCodeLoginScreenViewModel.mock(state: .displayCode(.verificationCode("123456")))
static var previews: some View {
QRCodeLoginScreen(context: initialStateViewModel.context)
.previewDisplayName("Initial")
@@ -367,19 +468,37 @@ struct QRCodeLoginScreen_Previews: PreviewProvider, TestablePreview {
QRCodeLoginScreen(context: invalidStateViewModel.context)
.previewDisplayName("Invalid")
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: 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: linkingUnsupportedStateViewModel.context)
.previewDisplayName("Linking unsupported")
QRCodeLoginScreen(context: cancelledStateViewModel.context)
.previewDisplayName("Cancelled")
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")
}
}
@@ -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<Data?>) {
_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)
// Start video capture.
DispatchQueue.global(qos: .userInitiated).async {
self.captureSession.startRunning()
}
}
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")
}
}
func stopScan() {
// Stop video capture.
DispatchQueue.global(qos: .userInitiated).async {
self.captureSession.stopRunning()
MXLog.info("QRCodeScannerView: capture session stopped")
}
}
}
@@ -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")
@@ -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)
@@ -42,7 +42,7 @@ enum AuthenticationServiceError: Error {
protocol AuthenticationServiceProxyProtocol {
/// The currently configured homeserver.
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { get }
/// Sets up the service for login on the specified homeserver address.
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError>
/// Performs login using OIDC for the current homeserver.
@@ -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
@@ -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
}
}
@@ -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<QrLoginProgress, Never>()
var qrLoginProgressPublisher: AnyPublisher<QrLoginProgress, Never> {
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<UserSessionProtocol, QRCodeLoginServiceError> {
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<UserSessionProtocol, QRCodeLoginServiceError> {
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
}
}
}
@@ -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<QrLoginProgress, Never> { get }
func loginWithQRCode(data: Data) async -> Result<UserSessionProtocol, QRCodeLoginServiceError>
}
@@ -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,
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:06198ff9cd5220d7c1282079371728698a9b9ac6d8058a4e09e90c4284b15658
size 118241
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:81808a0d38699a96abea059176bf065956bbbe2f457cb58bd4f8ec008e043807
size 240316
oid sha256:fb5249849da514c75ada263c8801339d7d23fe35d9e5e4096acb35a55b69b458
size 233785
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e8291c2203da7953c7b72e46b28ea7f3de99475fa5b89316cbdf1ddb14735f07
size 118456
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:32854697cdb9c9f4e84282193921cc0a2c959f34ab1bf6be2182ebaf06178935
size 156558
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:898bc820b2ddcf43903b67790fb4f33fd4cfe525360b3fd6daea043b0fa4bba3
size 121939
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5aafdb89352ede1e946d9462cad3885be7bcd87038e7194420a31549488cd45a
size 122996
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:608c132245379fca44da426c23d05e4322d0b5205266c27e8c7f2d702c10c6ab
size 175909
oid sha256:e762f2d946c682c59b7b6c1aaa56e5596bb64de67ec29eca6ef8b1ac92ad8d8c
size 178544
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:137d7d05b66b23ddb027f2d44fa31f3dadd35d5e1668f570c648833ea016d485
size 139013
oid sha256:64c7afdfb4698c972203b5987fcb9036b4aa75645969c22eefc54566a4e65747
size 139192
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:16c2e970674c36f0e40b52d1005d3e45dbf5f29123cb866aea643d8d5ea8b976
size 153497
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:19143f0cb869a917d7bff9d6c510ad5c589dad58f2f6a1f96c43402c8cb7eaca
size 130219
oid sha256:99707d136259b3fab5664ad24ca5923554f92d94c9e913be9d9b776e942b464d
size 124228
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c59ce9bc5b29e9b36d53f6dec406b4514f4d51cd65d34f544987078555936018
size 152784
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e532918bbc3dd6f19b1c6f595bf4e78328777f67ba168ca73820acf36c4b5fb3
size 364040
oid sha256:9f881dfeb084cbd409e9fdf7913252c6d1b841e2f875a86916d7c69dde989970
size 353783
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0cbe50417a3b6985b69a8465242d65af404dfccd919fc971aea864daaf4b6b47
size 150711
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:61c704f3d7b69ea192624ec6fb60fca7dfe6e089be632d4e22e72a3d1e13cdc6
size 208122
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fb425e78ece3246a1b290c8b84d1caa04d4246c86bf20eaa0b74c774eb618327
size 151075
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5af5a51a6ae879671bfdf417d9f9cc7715e20512d85379373f9e8104b4c2ee4c
size 163873
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0b26435b6414ee0c2782fe084939075016fd26533fc9af16ed7645dd2f3a1347
size 256639
oid sha256:c1cd2940d92227d7036640f6ea64eddd4f6245ab04662eee67b50e1162e09a9d
size 262484
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:82fa86b47675cb62a236b024948037afca1cde901a22605791d2a91cd3c79b56
size 174326
oid sha256:446735c3777cd614aa008beae3b0e3fc744e6c4cc44ac7f78dd082a9b02d7625
size 174596
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:59fda8126fd1881e419aea528308604efadaefa1b18679e5f263ba3f84f14d2e
size 220251
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d2fc9f0ebae6cf4c1521ffbde06fbdad3bc15071db2a9461abf1b7d1415f113e
size 164912
oid sha256:598203104fef16dfa93e6d77b1529892774c0be30f95344ff800b7c64698b6ac
size 153117
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1428741d616b4cdb28973473c2602392a9c2ca0ef414e90fa40d110a1ed29ef6
size 71029
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9d2b9794ad3b7d038113d0b87bd0f694194471adb29357e963f8143d955dc4ac
size 164424
oid sha256:eca687f56b650ef5ac41219061a45dac5b56d9eaff54da40e869162ced207d08
size 159528
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c1ab0cb4abb4cad384c95d5b788434a8e6a3449fac4e6bd0d73a6cfc8b0bf55f
size 68137
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4b42499314b0f777f32878783bb6ba28009da920c74a08b69e21f55d1677085c
size 94623
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:53f9ce852ff302cc2b9cc52d7d057becda1ae34eefc4cc8cfd98631e9d249c6a
size 72003
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9d531f55191cb88f6678f6950a26d9f3eb382d95c41046c49d50a8c78724b9f1
size 72077
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8e19a0e1e323537e6a70da304e0be9bbb11643371793514597e022e0301732ca
size 124161
oid sha256:21d7f98e4f6c9b25aa5a812a5410b6fe2fb4f202b1e556514554ff6afe7e200a
size 126332
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ed2670440f91d41f983081d877f7c084ca205724143f5b5d37dc6fdb52fc188c
size 78633
oid sha256:8f4efd9d313010c3961beb4ad8f85c7179cfe17c0900bf6a3150ca1070670f0f
size 78648
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:60d9429395be091a64a79de2bd8d03152777bbf9ac5dd353e1f0fe4a1406c04f
size 101877
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4a70dec75d62f4ebb3d00f613c3314c741bef3cc757d5c9b78358ca8b9535d52
size 78473
oid sha256:6dabb5a3e2d2f084d783367321ae139da12a3d59cc2cd908a11fe5c27cc57e21
size 73675
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:986eabb153ea07fd2caa7239fad835b13363674b67e3d40f743496eeea6c83ba
size 97338
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:54b0bc2bacf2504febf374970d5afdbf8f8f03b139005c5c81d04761faf66132
size 268685
oid sha256:b02fa3348fb1bc56e1bd514322d157fae8247228239d20a369a178fe10f2eea8
size 260577
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:73ffa2cb86e0228ea0788e53af38e3d3fc2aa1e14d5b3d1358dfc4bc3f5f474b
size 95518
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:be6ad96a182495c28b889d2c1a6d2456f1cf27b2f443cf0ee6a93d079bad80be
size 141938
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a463d00012f4f3ea2bf35e87c6b35e425ba9865ae77b2852735b1f4c7c02627d
size 97564
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:db8e449dc3993188507e23f79de263ed15a9e75188d1e70ecc5370a444bbdebd
size 103468
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c1f4e2f5924db1858c2417235b24e3333c3570622b02a85ec1e54e1bcf9504c
size 194638
oid sha256:a412452ca11c6e7c53d7ad4e01faa8c4bc3979771912c2b96b13422cef30d9bd
size 199595
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a1f5a01424b85977592a295500142b1ebf3c6d0720c45b03334f108f1bc03f1e
size 114199
oid sha256:42f429a79217b6da6f567ad9e9c7cc365e65fe005ad4773bbf370998420f9e80
size 114340
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6b479b466cc5b987468386a09286428f4105ade82f27296c194d4950bf7f28bf
size 157737
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5d7c1eb55621a0c337ecc6c7882fdd6c48d55ca6ba9bceaaa7e334dc123b0b1e
size 111028
oid sha256:9743e035cb1d249ce7d4d9f7602d01d35d2887eb614a7cffb34d8abc357144ef
size 102855
@@ -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)
}
}
@@ -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<QrLoginProgress, Never>!
private var qrServiceMock: QRCodeLoginServiceMock!
private var appMediatorMock: AppMediatorMock!
private var viewModel: QRCodeLoginScreenViewModelProtocol!
private var context: QRCodeLoginScreenViewModelType.Context {
viewModel.context
}
override func setUp() {
qrProgressSubject = PassthroughSubject<QrLoginProgress, Never>()
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()
}
}
+1 -1
View File
@@ -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