mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-6
@@ -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,
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:06198ff9cd5220d7c1282079371728698a9b9ac6d8058a4e09e90c4284b15658
|
||||
size 118241
|
||||
+2
-2
@@ -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
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:32854697cdb9c9f4e84282193921cc0a2c959f34ab1bf6be2182ebaf06178935
|
||||
size 156558
|
||||
+3
@@ -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
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:608c132245379fca44da426c23d05e4322d0b5205266c27e8c7f2d702c10c6ab
|
||||
size 175909
|
||||
oid sha256:e762f2d946c682c59b7b6c1aaa56e5596bb64de67ec29eca6ef8b1ac92ad8d8c
|
||||
size 178544
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:137d7d05b66b23ddb027f2d44fa31f3dadd35d5e1668f570c648833ea016d485
|
||||
size 139013
|
||||
oid sha256:64c7afdfb4698c972203b5987fcb9036b4aa75645969c22eefc54566a4e65747
|
||||
size 139192
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:16c2e970674c36f0e40b52d1005d3e45dbf5f29123cb866aea643d8d5ea8b976
|
||||
size 153497
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:19143f0cb869a917d7bff9d6c510ad5c589dad58f2f6a1f96c43402c8cb7eaca
|
||||
size 130219
|
||||
oid sha256:99707d136259b3fab5664ad24ca5923554f92d94c9e913be9d9b776e942b464d
|
||||
size 124228
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c59ce9bc5b29e9b36d53f6dec406b4514f4d51cd65d34f544987078555936018
|
||||
size 152784
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e532918bbc3dd6f19b1c6f595bf4e78328777f67ba168ca73820acf36c4b5fb3
|
||||
size 364040
|
||||
oid sha256:9f881dfeb084cbd409e9fdf7913252c6d1b841e2f875a86916d7c69dde989970
|
||||
size 353783
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0cbe50417a3b6985b69a8465242d65af404dfccd919fc971aea864daaf4b6b47
|
||||
size 150711
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:61c704f3d7b69ea192624ec6fb60fca7dfe6e089be632d4e22e72a3d1e13cdc6
|
||||
size 208122
|
||||
+3
@@ -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
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0b26435b6414ee0c2782fe084939075016fd26533fc9af16ed7645dd2f3a1347
|
||||
size 256639
|
||||
oid sha256:c1cd2940d92227d7036640f6ea64eddd4f6245ab04662eee67b50e1162e09a9d
|
||||
size 262484
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:82fa86b47675cb62a236b024948037afca1cde901a22605791d2a91cd3c79b56
|
||||
size 174326
|
||||
oid sha256:446735c3777cd614aa008beae3b0e3fc744e6c4cc44ac7f78dd082a9b02d7625
|
||||
size 174596
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:59fda8126fd1881e419aea528308604efadaefa1b18679e5f263ba3f84f14d2e
|
||||
size 220251
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d2fc9f0ebae6cf4c1521ffbde06fbdad3bc15071db2a9461abf1b7d1415f113e
|
||||
size 164912
|
||||
oid sha256:598203104fef16dfa93e6d77b1529892774c0be30f95344ff800b7c64698b6ac
|
||||
size 153117
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1428741d616b4cdb28973473c2602392a9c2ca0ef414e90fa40d110a1ed29ef6
|
||||
size 71029
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9d2b9794ad3b7d038113d0b87bd0f694194471adb29357e963f8143d955dc4ac
|
||||
size 164424
|
||||
oid sha256:eca687f56b650ef5ac41219061a45dac5b56d9eaff54da40e869162ced207d08
|
||||
size 159528
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c1ab0cb4abb4cad384c95d5b788434a8e6a3449fac4e6bd0d73a6cfc8b0bf55f
|
||||
size 68137
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4b42499314b0f777f32878783bb6ba28009da920c74a08b69e21f55d1677085c
|
||||
size 94623
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:53f9ce852ff302cc2b9cc52d7d057becda1ae34eefc4cc8cfd98631e9d249c6a
|
||||
size 72003
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9d531f55191cb88f6678f6950a26d9f3eb382d95c41046c49d50a8c78724b9f1
|
||||
size 72077
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8e19a0e1e323537e6a70da304e0be9bbb11643371793514597e022e0301732ca
|
||||
size 124161
|
||||
oid sha256:21d7f98e4f6c9b25aa5a812a5410b6fe2fb4f202b1e556514554ff6afe7e200a
|
||||
size 126332
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed2670440f91d41f983081d877f7c084ca205724143f5b5d37dc6fdb52fc188c
|
||||
size 78633
|
||||
oid sha256:8f4efd9d313010c3961beb4ad8f85c7179cfe17c0900bf6a3150ca1070670f0f
|
||||
size 78648
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60d9429395be091a64a79de2bd8d03152777bbf9ac5dd353e1f0fe4a1406c04f
|
||||
size 101877
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4a70dec75d62f4ebb3d00f613c3314c741bef3cc757d5c9b78358ca8b9535d52
|
||||
size 78473
|
||||
oid sha256:6dabb5a3e2d2f084d783367321ae139da12a3d59cc2cd908a11fe5c27cc57e21
|
||||
size 73675
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:986eabb153ea07fd2caa7239fad835b13363674b67e3d40f743496eeea6c83ba
|
||||
size 97338
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:54b0bc2bacf2504febf374970d5afdbf8f8f03b139005c5c81d04761faf66132
|
||||
size 268685
|
||||
oid sha256:b02fa3348fb1bc56e1bd514322d157fae8247228239d20a369a178fe10f2eea8
|
||||
size 260577
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:73ffa2cb86e0228ea0788e53af38e3d3fc2aa1e14d5b3d1358dfc4bc3f5f474b
|
||||
size 95518
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:be6ad96a182495c28b889d2c1a6d2456f1cf27b2f443cf0ee6a93d079bad80be
|
||||
size 141938
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a463d00012f4f3ea2bf35e87c6b35e425ba9865ae77b2852735b1f4c7c02627d
|
||||
size 97564
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:db8e449dc3993188507e23f79de263ed15a9e75188d1e70ecc5370a444bbdebd
|
||||
size 103468
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c1f4e2f5924db1858c2417235b24e3333c3570622b02a85ec1e54e1bcf9504c
|
||||
size 194638
|
||||
oid sha256:a412452ca11c6e7c53d7ad4e01faa8c4bc3979771912c2b96b13422cef30d9bd
|
||||
size 199595
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a1f5a01424b85977592a295500142b1ebf3c6d0720c45b03334f108f1bc03f1e
|
||||
size 114199
|
||||
oid sha256:42f429a79217b6da6f567ad9e9c7cc365e65fe005ad4773bbf370998420f9e80
|
||||
size 114340
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6b479b466cc5b987468386a09286428f4105ade82f27296c194d4950bf7f28bf
|
||||
size 157737
|
||||
+2
-2
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user