Configure the AuthenticationService later now that we have 2 flows on the start screen. (#3316)

* Don't query the homeserver until confirming it (or selecting a different one).

* Setup the infrastructure to test AuthenticationService.

Implement basic tests for configuration & password login.

* Use the real AuthenticationService with a mock Client in all of the tests.

* Add tests for the ServerConfirmationScreenViewModel.

* Remove redundant view state and test for it.
This commit is contained in:
Doug 2024-09-25 14:40:18 +01:00 committed by GitHub
parent af8c16150b
commit a8dbda90d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 1209 additions and 298 deletions

View File

@ -83,6 +83,7 @@
0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; };
0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; };
0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */; }; 0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */; };
0F4709282FCCFBEFED427B8A /* AuthenticationClientBuilderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4760CE2128FBC217304272AB /* AuthenticationClientBuilderMock.swift */; };
0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */; }; 0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */; };
108D3C0707A90B0F848CDBB9 /* ResolveVerifiedUserSendFailureScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60011EF0086E49DBD78E16E5 /* ResolveVerifiedUserSendFailureScreenModels.swift */; }; 108D3C0707A90B0F848CDBB9 /* ResolveVerifiedUserSendFailureScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60011EF0086E49DBD78E16E5 /* ResolveVerifiedUserSendFailureScreenModels.swift */; };
109AEB7D33C4497727AFB87F /* TimelineInteractionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BA894BC09972DC45E497D37 /* TimelineInteractionHandler.swift */; }; 109AEB7D33C4497727AFB87F /* TimelineInteractionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BA894BC09972DC45E497D37 /* TimelineInteractionHandler.swift */; };
@ -150,6 +151,7 @@
206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; }; 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; };
208C19811613F9A10F8A7B75 /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; }; 208C19811613F9A10F8A7B75 /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; };
20C16A3F718802B0E4A19C83 /* URLComponentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76310030C831D4610A705603 /* URLComponentsTests.swift */; }; 20C16A3F718802B0E4A19C83 /* URLComponentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76310030C831D4610A705603 /* URLComponentsTests.swift */; };
210DB40676DF2A23E69C2D06 /* AuthenticationClientBuilderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B655A536341D2695158C6664 /* AuthenticationClientBuilderFactory.swift */; };
2118E35D312951B241067BD5 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345172AD4377E83A44BD864F /* MessageComposerTextField.swift */; }; 2118E35D312951B241067BD5 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345172AD4377E83A44BD864F /* MessageComposerTextField.swift */; };
211B5F524E851178EE549417 /* CurrentValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */; }; 211B5F524E851178EE549417 /* CurrentValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */; };
21813AF91CFC6F3E3896DB53 /* AppLockSetupBiometricsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F130DF775CE6BC51A4E392 /* AppLockSetupBiometricsScreenModels.swift */; }; 21813AF91CFC6F3E3896DB53 /* AppLockSetupBiometricsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F130DF775CE6BC51A4E392 /* AppLockSetupBiometricsScreenModels.swift */; };
@ -225,7 +227,6 @@
3116693C5EB476E028990416 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74611A4182DCF5F4D42696EC /* XCTestCase.swift */; }; 3116693C5EB476E028990416 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74611A4182DCF5F4D42696EC /* XCTestCase.swift */; };
3118D9ABFD4BE5A3492FF88A /* ElementCallConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC437C491EA6996513B1CEAB /* ElementCallConfiguration.swift */; }; 3118D9ABFD4BE5A3492FF88A /* ElementCallConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC437C491EA6996513B1CEAB /* ElementCallConfiguration.swift */; };
32B7891D937377A59606EDFC /* UserFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DD8599815136EFF5B73F38 /* UserFlowTests.swift */; }; 32B7891D937377A59606EDFC /* UserFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DD8599815136EFF5B73F38 /* UserFlowTests.swift */; };
32FC143630CE22A9E403370B /* MockAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */; };
339BC18777912E1989F2F17D /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584A61D9C459FAFEF038A7C0 /* Section.swift */; }; 339BC18777912E1989F2F17D /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584A61D9C459FAFEF038A7C0 /* Section.swift */; };
33CAC1226DFB8B5D8447D286 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; }; 33CAC1226DFB8B5D8447D286 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; };
33F1FB19F222BA9930AB1A00 /* RoomListFiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6372DD10DED30E7AD7BCE21 /* RoomListFiltersView.swift */; }; 33F1FB19F222BA9930AB1A00 /* RoomListFiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6372DD10DED30E7AD7BCE21 /* RoomListFiltersView.swift */; };
@ -290,6 +291,7 @@
407DCE030E0F9B7C9861D38A /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 1081D3630AAD3ACEDDEC3A98 /* LRUCache */; }; 407DCE030E0F9B7C9861D38A /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 1081D3630AAD3ACEDDEC3A98 /* LRUCache */; };
40B79D20A873620F7F128A2C /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FA991289149D31F4286747 /* UserPreference.swift */; }; 40B79D20A873620F7F128A2C /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FA991289149D31F4286747 /* UserPreference.swift */; };
414F50CFCFEEE2611127DCFB /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; }; 414F50CFCFEEE2611127DCFB /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; };
41C5DA0C06F30311A221E85B /* ClientSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */; };
41CE5E1289C8768FC5B6490C /* RoomTimelineItemViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2D52E36AD614B3C003EF6 /* RoomTimelineItemViewState.swift */; }; 41CE5E1289C8768FC5B6490C /* RoomTimelineItemViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2D52E36AD614B3C003EF6 /* RoomTimelineItemViewState.swift */; };
41DFDD212D1BE57CA50D783B /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = 81DB3AB6CE996AB3954F4F03 /* KZFileWatchers */; }; 41DFDD212D1BE57CA50D783B /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = 81DB3AB6CE996AB3954F4F03 /* KZFileWatchers */; };
41F553349AF44567184822D8 /* APNSPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D670124FC3E84F23A62CCF /* APNSPayload.swift */; }; 41F553349AF44567184822D8 /* APNSPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D670124FC3E84F23A62CCF /* APNSPayload.swift */; };
@ -405,10 +407,10 @@
5AE6404C4FD4848ACCFF9EDC /* SecureBackupLogoutConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1573D28C8A9FB6399D0EEFB /* SecureBackupLogoutConfirmationScreenCoordinator.swift */; }; 5AE6404C4FD4848ACCFF9EDC /* SecureBackupLogoutConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1573D28C8A9FB6399D0EEFB /* SecureBackupLogoutConfirmationScreenCoordinator.swift */; };
5B6E5AD224509E6C0B520D6E /* RoomMemberDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */; }; 5B6E5AD224509E6C0B520D6E /* RoomMemberDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */; };
5B7D24A318AFF75AD611A026 /* RoomDirectorySearchScreenScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */; }; 5B7D24A318AFF75AD611A026 /* RoomDirectorySearchScreenScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */; };
5BBDF9926CB645DE2F7BC258 /* EventTimelineItemSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86D447D771CEF6194348F5F /* EventTimelineItemSDKMock.swift */; };
5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */; }; 5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */; };
5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; }; 5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; };
5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */; }; 5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */; };
5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */; };
5D27B6537591471A42C89027 /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */; }; 5D27B6537591471A42C89027 /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */; };
5D52925FEB1B780C65B0529F /* PinnedEventsTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4F6D7000EDCD187E0989E7 /* PinnedEventsTimelineScreen.swift */; }; 5D52925FEB1B780C65B0529F /* PinnedEventsTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4F6D7000EDCD187E0989E7 /* PinnedEventsTimelineScreen.swift */; };
5D53AE9342A4C06B704247ED /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; }; 5D53AE9342A4C06B704247ED /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; };
@ -541,6 +543,7 @@
79741C1953269FF1A211D246 /* RoomPollsHistoryScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E14FF533D25A0692F7CEB0 /* RoomPollsHistoryScreenViewModel.swift */; }; 79741C1953269FF1A211D246 /* RoomPollsHistoryScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E14FF533D25A0692F7CEB0 /* RoomPollsHistoryScreenViewModel.swift */; };
798BF3072137833FBD3F4C96 /* TimelineDeliveryStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */; }; 798BF3072137833FBD3F4C96 /* TimelineDeliveryStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */; };
79959F8E45C3749997482A7F /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */; }; 79959F8E45C3749997482A7F /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */; };
79D57E9AE03A2DC689D14EA2 /* UserSessionStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB9BA2F30EB8C33226D8FF1 /* UserSessionStoreMock.swift */; };
7A02EB29F3B993AB20E0A198 /* RoomPollsHistoryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */; }; 7A02EB29F3B993AB20E0A198 /* RoomPollsHistoryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */; };
7A0D335D38ECA095A575B4F7 /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB0E533508094156D8024C3 /* TimelineStyler.swift */; }; 7A0D335D38ECA095A575B4F7 /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB0E533508094156D8024C3 /* TimelineStyler.swift */; };
7A170A5A4A352954BB2A1B96 /* AuthenticationStartScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E8C8817F59BEC7E358EB78 /* AuthenticationStartScreen.swift */; }; 7A170A5A4A352954BB2A1B96 /* AuthenticationStartScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E8C8817F59BEC7E358EB78 /* AuthenticationStartScreen.swift */; };
@ -655,6 +658,7 @@
915B4CDAF220D9AEB4047D45 /* PollInteractionHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */; }; 915B4CDAF220D9AEB4047D45 /* PollInteractionHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */; };
91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; }; 91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; };
91C6AC0E9D2B9C0C76CC6AD4 /* RoomDirectorySearchScreenScreenModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3984C93B8E9B10C92DADF9EE /* RoomDirectorySearchScreenScreenModelProtocol.swift */; }; 91C6AC0E9D2B9C0C76CC6AD4 /* RoomDirectorySearchScreenScreenModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3984C93B8E9B10C92DADF9EE /* RoomDirectorySearchScreenScreenModelProtocol.swift */; };
92012C96039BC8C2CAEBA9E2 /* AuthenticationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671C338B7259DC5774816885 /* AuthenticationServiceTests.swift */; };
9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; }; 9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; };
92720AB0DA9AB5EEF1DAF56B /* SecureBackupLogoutConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC017C3CB6B0F7C63F460F2 /* SecureBackupLogoutConfirmationScreenViewModel.swift */; }; 92720AB0DA9AB5EEF1DAF56B /* SecureBackupLogoutConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC017C3CB6B0F7C63F460F2 /* SecureBackupLogoutConfirmationScreenViewModel.swift */; };
9278EC51D24E57445B290521 /* AudioSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */; }; 9278EC51D24E57445B290521 /* AudioSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */; };
@ -749,6 +753,7 @@
A4B123C635F70DDD4BC2FAC9 /* BlockedUsersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E76A706B3EEA32B882DA5E2D /* BlockedUsersScreenViewModelProtocol.swift */; }; A4B123C635F70DDD4BC2FAC9 /* BlockedUsersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E76A706B3EEA32B882DA5E2D /* BlockedUsersScreenViewModelProtocol.swift */; };
A4C29D373986AFE4559696D5 /* SecureBackupKeyBackupScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4525E8C0FBDD27D1ACE90952 /* SecureBackupKeyBackupScreenViewModelProtocol.swift */; }; A4C29D373986AFE4559696D5 /* SecureBackupKeyBackupScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4525E8C0FBDD27D1ACE90952 /* SecureBackupKeyBackupScreenViewModelProtocol.swift */; };
A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; }; A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; };
A51C65E5A3C9F2464A91A380 /* AuthenticationClientBuilderFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */; };
A52090A4FE0DB826578DFC03 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0724EBDFE8BB4C9E5547C57D /* Client.swift */; }; A52090A4FE0DB826578DFC03 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0724EBDFE8BB4C9E5547C57D /* Client.swift */; };
A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; }; A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; };
A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; }; A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; };
@ -1054,7 +1059,6 @@
EDF8919F15DE0FF00EF99E70 /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */; }; EDF8919F15DE0FF00EF99E70 /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */; };
EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */; }; EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */; };
EE56238683BC3ECA9BA00684 /* GlobalSearchScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4D639E27D5882A6A71AECF /* GlobalSearchScreenViewModelTests.swift */; }; EE56238683BC3ECA9BA00684 /* GlobalSearchScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4D639E27D5882A6A71AECF /* GlobalSearchScreenViewModelTests.swift */; };
EE57A96130DD8DB053790AE2 /* EventTimelineItemSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7A6BBC686B1F840FA807FB /* EventTimelineItemSDKMock.swift */; };
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; };
EEAE954289DE813A61656AE0 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; }; EEAE954289DE813A61656AE0 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; };
EEB9C1555C63B93CA9C372C2 /* EmojiPickerScreenHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5E29E9A22F45534FBD5B58 /* EmojiPickerScreenHeaderView.swift */; }; EEB9C1555C63B93CA9C372C2 /* EmojiPickerScreenHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5E29E9A22F45534FBD5B58 /* EmojiPickerScreenHeaderView.swift */; };
@ -1219,6 +1223,7 @@
052B2F924572AFD70B5F500E /* StartChatScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModel.swift; sourceTree = "<group>"; }; 052B2F924572AFD70B5F500E /* StartChatScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModel.swift; sourceTree = "<group>"; };
054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionUITests.swift; sourceTree = "<group>"; }; 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionUITests.swift; sourceTree = "<group>"; };
05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRecoveryKeyConfirmationBanner.swift; sourceTree = "<group>"; }; 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRecoveryKeyConfirmationBanner.swift; sourceTree = "<group>"; };
0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilderFactoryMock.swift; sourceTree = "<group>"; };
05596E4A11A8C9346E9E54AE /* SoftLogoutScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenCoordinator.swift; sourceTree = "<group>"; }; 05596E4A11A8C9346E9E54AE /* SoftLogoutScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenCoordinator.swift; sourceTree = "<group>"; };
05A3E8741D199CD1A37F4CBF /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; }; 05A3E8741D199CD1A37F4CBF /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
05AF58372CA884A789EB9C5A /* AppMediatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediatorProtocol.swift; sourceTree = "<group>"; }; 05AF58372CA884A789EB9C5A /* AppMediatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediatorProtocol.swift; sourceTree = "<group>"; };
@ -1311,7 +1316,6 @@
1BA5A62DA4B543827FF82354 /* LAContextMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LAContextMock.swift; sourceTree = "<group>"; }; 1BA5A62DA4B543827FF82354 /* LAContextMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LAContextMock.swift; sourceTree = "<group>"; };
1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerProtocol.swift; sourceTree = "<group>"; }; 1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerProtocol.swift; sourceTree = "<group>"; };
1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = "<group>"; }; 1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = "<group>"; };
1C7A6BBC686B1F840FA807FB /* EventTimelineItemSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItemSDKMock.swift; sourceTree = "<group>"; };
1C7F63EB1525E697CAEB002B /* BlankFormCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlankFormCoordinator.swift; sourceTree = "<group>"; }; 1C7F63EB1525E697CAEB002B /* BlankFormCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlankFormCoordinator.swift; sourceTree = "<group>"; };
1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreen.swift; sourceTree = "<group>"; }; 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreen.swift; sourceTree = "<group>"; };
1CD7C0A2750998C2D77AD00F /* JoinRoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModel.swift; sourceTree = "<group>"; }; 1CD7C0A2750998C2D77AD00F /* JoinRoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModel.swift; sourceTree = "<group>"; };
@ -1323,7 +1327,6 @@
1D9F148717D74F73BE724434 /* LongPressWithFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPressWithFeedback.swift; sourceTree = "<group>"; }; 1D9F148717D74F73BE724434 /* LongPressWithFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPressWithFeedback.swift; sourceTree = "<group>"; };
1DA7E93C2E148B96EF6A8500 /* TimelineItemAccessibilityModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemAccessibilityModifier.swift; sourceTree = "<group>"; }; 1DA7E93C2E148B96EF6A8500 /* TimelineItemAccessibilityModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemAccessibilityModifier.swift; sourceTree = "<group>"; };
1DB2FC2AA9A07EE792DF65CF /* NotificationPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenModels.swift; sourceTree = "<group>"; }; 1DB2FC2AA9A07EE792DF65CF /* NotificationPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenModels.swift; sourceTree = "<group>"; };
1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = "<group>"; };
1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenModels.swift; sourceTree = "<group>"; }; 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenModels.swift; sourceTree = "<group>"; };
1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelProtocol.swift; sourceTree = "<group>"; };
1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProposedViewSize.swift; sourceTree = "<group>"; }; 1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProposedViewSize.swift; sourceTree = "<group>"; };
@ -1519,6 +1522,7 @@
471BB7276C97AF60B3A5463B /* RoomDirectorySearchProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxy.swift; sourceTree = "<group>"; }; 471BB7276C97AF60B3A5463B /* RoomDirectorySearchProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxy.swift; sourceTree = "<group>"; };
475D47D0BFE961B02BAC5D49 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 475D47D0BFE961B02BAC5D49 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
475EB595D7527E9A8A14043E /* uz */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uz; path = uz.lproj/Localizable.strings; sourceTree = "<group>"; }; 475EB595D7527E9A8A14043E /* uz */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uz; path = uz.lproj/Localizable.strings; sourceTree = "<group>"; };
4760CE2128FBC217304272AB /* AuthenticationClientBuilderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilderMock.swift; sourceTree = "<group>"; };
47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = "<group>"; }; 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = "<group>"; };
47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = "<group>"; }; 47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = "<group>"; };
@ -1646,6 +1650,7 @@
66AFD800AF033D8B0D11191A /* UserPropertiesExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPropertiesExt.swift; sourceTree = "<group>"; }; 66AFD800AF033D8B0D11191A /* UserPropertiesExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPropertiesExt.swift; sourceTree = "<group>"; };
66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = "<group>"; }; 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = "<group>"; };
66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = "<group>"; }; 66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = "<group>"; };
671C338B7259DC5774816885 /* AuthenticationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceTests.swift; sourceTree = "<group>"; };
6722709BD6178E10B70C9641 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/SAS.strings; sourceTree = "<group>"; }; 6722709BD6178E10B70C9641 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/SAS.strings; sourceTree = "<group>"; };
68010886142843705E342645 /* ProgressMaskModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressMaskModifier.swift; sourceTree = "<group>"; }; 68010886142843705E342645 /* ProgressMaskModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressMaskModifier.swift; sourceTree = "<group>"; };
6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = "<group>"; }; 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = "<group>"; };
@ -1805,6 +1810,7 @@
8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorMock.swift; sourceTree = "<group>"; }; 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorMock.swift; sourceTree = "<group>"; };
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; }; 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = "<group>"; }; 8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = "<group>"; };
8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSDKMock.swift; sourceTree = "<group>"; };
8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; 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>"; }; 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>"; }; 8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenViewModelTests.swift; sourceTree = "<group>"; };
@ -1871,6 +1877,7 @@
9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModel.swift; sourceTree = "<group>"; }; 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModel.swift; sourceTree = "<group>"; };
9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachment.swift; sourceTree = "<group>"; }; 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachment.swift; sourceTree = "<group>"; };
9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = "<group>"; }; 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = "<group>"; };
9EB9BA2F30EB8C33226D8FF1 /* UserSessionStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreMock.swift; sourceTree = "<group>"; };
9ECF11669EF253E98AA2977A /* CompletionSuggestionServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionServiceProtocol.swift; sourceTree = "<group>"; }; 9ECF11669EF253E98AA2977A /* CompletionSuggestionServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionServiceProtocol.swift; sourceTree = "<group>"; };
9F1DF3FFFE5ED2B8133F43A7 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = "<group>"; }; 9F1DF3FFFE5ED2B8133F43A7 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = "<group>"; };
9F40FB0A43DAECEC27C73722 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/SAS.strings; sourceTree = "<group>"; }; 9F40FB0A43DAECEC27C73722 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/SAS.strings; sourceTree = "<group>"; };
@ -1981,6 +1988,7 @@
B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = "<group>"; }; B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = "<group>"; };
B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.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>"; }; B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetails.swift; sourceTree = "<group>"; };
B655A536341D2695158C6664 /* AuthenticationClientBuilderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilderFactory.swift; sourceTree = "<group>"; };
B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHooks.swift; sourceTree = "<group>"; }; B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHooks.swift; sourceTree = "<group>"; };
B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; }; B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
B70A50C41C5871B4DB905E7E /* VoiceMessageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomTimelineView.swift; sourceTree = "<group>"; }; B70A50C41C5871B4DB905E7E /* VoiceMessageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomTimelineView.swift; sourceTree = "<group>"; };
@ -1994,6 +2002,7 @@
B81B6170DB690013CEB646F4 /* MapLibreModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreModels.swift; sourceTree = "<group>"; }; B81B6170DB690013CEB646F4 /* MapLibreModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreModels.swift; sourceTree = "<group>"; };
B8516302ACCA94A0E680AB3B /* VoiceMessageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageButton.swift; sourceTree = "<group>"; }; B8516302ACCA94A0E680AB3B /* VoiceMessageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageButton.swift; sourceTree = "<group>"; };
B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = "<group>"; }; B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = "<group>"; };
B86D447D771CEF6194348F5F /* EventTimelineItemSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItemSDKMock.swift; sourceTree = "<group>"; };
B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = "<group>"; }; B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = "<group>"; };
B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = "<group>"; }; B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = "<group>"; };
B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = "<group>"; }; B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = "<group>"; };
@ -2138,7 +2147,6 @@
D95E8C0EFEC0C6F96EDAA71A /* PreviewTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = PreviewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D95E8C0EFEC0C6F96EDAA71A /* PreviewTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = PreviewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
DA14564EE143F73F7E4D1F79 /* RoomNotificationSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenModels.swift; sourceTree = "<group>"; }; DA14564EE143F73F7E4D1F79 /* RoomNotificationSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenModels.swift; sourceTree = "<group>"; };
DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenUITests.swift; sourceTree = "<group>"; }; DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenUITests.swift; sourceTree = "<group>"; };
DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationService.swift; sourceTree = "<group>"; };
DA3D82522494E78746B2214E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/SAS.strings; sourceTree = "<group>"; }; DA3D82522494E78746B2214E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/SAS.strings; sourceTree = "<group>"; };
DAB8D7926A5684E18196B538 /* VoiceMessageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCache.swift; sourceTree = "<group>"; }; DAB8D7926A5684E18196B538 /* VoiceMessageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCache.swift; sourceTree = "<group>"; };
DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreen.swift; sourceTree = "<group>"; }; DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreen.swift; sourceTree = "<group>"; };
@ -2887,10 +2895,11 @@
children = ( children = (
69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */, 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */,
3BAC027034248429A438886B /* AppMediatorMock.swift */, 3BAC027034248429A438886B /* AppMediatorMock.swift */,
0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */,
4760CE2128FBC217304272AB /* AuthenticationClientBuilderMock.swift */,
E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */, E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */,
4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */, 4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */,
E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */, E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */,
1C7A6BBC686B1F840FA807FB /* EventTimelineItemSDKMock.swift */,
867DC9530C42F7B5176BE465 /* JoinedRoomProxyMock.swift */, 867DC9530C42F7B5176BE465 /* JoinedRoomProxyMock.swift */,
8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */, 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */,
382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */, 382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */,
@ -2907,7 +2916,9 @@
7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */, 7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */,
AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */, AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */,
F4469F6AE311BDC439B3A5EC /* UserSessionMock.swift */, F4469F6AE311BDC439B3A5EC /* UserSessionMock.swift */,
9EB9BA2F30EB8C33226D8FF1 /* UserSessionStoreMock.swift */,
B23135B06B044CB811139D2F /* Generated */, B23135B06B044CB811139D2F /* Generated */,
E5E545F92D01588360A9BAC5 /* SDK */,
); );
path = Mocks; path = Mocks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3790,6 +3801,7 @@
89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */, 89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */,
C55CC239AE12339C565F6C9A /* AudioRecorderStateTests.swift */, C55CC239AE12339C565F6C9A /* AudioRecorderStateTests.swift */,
2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */, 2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */,
671C338B7259DC5774816885 /* AuthenticationServiceTests.swift */,
8FB89DC7F9A4A91020037001 /* AuthenticationStartScreenViewModelTests.swift */, 8FB89DC7F9A4A91020037001 /* AuthenticationStartScreenViewModelTests.swift */,
93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */, 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */,
240610DF32F3213BEC5611D7 /* BlockedUsersScreenViewModelTests.swift */, 240610DF32F3213BEC5611D7 /* BlockedUsersScreenViewModelTests.swift */,
@ -4405,7 +4417,6 @@
295E28C3B9EAADF519BF2F44 /* AuthenticationFlowCoordinatorUITests.swift */, 295E28C3B9EAADF519BF2F44 /* AuthenticationFlowCoordinatorUITests.swift */,
C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */,
F8CEB4634C0DD7779C4AB504 /* CreateRoomScreenUITests.swift */, F8CEB4634C0DD7779C4AB504 /* CreateRoomScreenUITests.swift */,
1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */,
3368395F06AA180138E185B6 /* PollFormScreenUITests.swift */, 3368395F06AA180138E185B6 /* PollFormScreenUITests.swift */,
C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */, C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */,
45571C2EBD98ED7E0CEA7AF7 /* RoomRolesAndPermissionsUITests.swift */, 45571C2EBD98ED7E0CEA7AF7 /* RoomRolesAndPermissionsUITests.swift */,
@ -4651,9 +4662,9 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0F569CFB77E0D40BD82203D9 /* AuthenticationClientBuilder.swift */, 0F569CFB77E0D40BD82203D9 /* AuthenticationClientBuilder.swift */,
B655A536341D2695158C6664 /* AuthenticationClientBuilderFactory.swift */,
F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */, F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */,
5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */, 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */,
DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */,
A69869844D2B6F5BD9AABF85 /* OIDCConfigurationProxy.swift */, A69869844D2B6F5BD9AABF85 /* OIDCConfigurationProxy.swift */,
); );
path = Authentication; path = Authentication;
@ -5243,6 +5254,15 @@
path = Screens; path = Screens;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E5E545F92D01588360A9BAC5 /* SDK */ = {
isa = PBXGroup;
children = (
8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */,
B86D447D771CEF6194348F5F /* EventTimelineItemSDKMock.swift */,
);
path = SDK;
sourceTree = "<group>";
};
E600AACDF87CDBCE32683236 /* Resources */ = { E600AACDF87CDBCE32683236 /* Resources */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -6072,6 +6092,7 @@
C1429699A6A5BB09A25775C1 /* AudioPlayerStateTests.swift in Sources */, C1429699A6A5BB09A25775C1 /* AudioPlayerStateTests.swift in Sources */,
3042527CB344A9EF1157FC26 /* AudioRecorderStateTests.swift in Sources */, 3042527CB344A9EF1157FC26 /* AudioRecorderStateTests.swift in Sources */,
192A3CDCD0174AD1E4A128E4 /* AudioRecorderTests.swift in Sources */, 192A3CDCD0174AD1E4A128E4 /* AudioRecorderTests.swift in Sources */,
92012C96039BC8C2CAEBA9E2 /* AuthenticationServiceTests.swift in Sources */,
8ED8AF57A06F5EE9978ED23F /* AuthenticationStartScreenViewModelTests.swift in Sources */, 8ED8AF57A06F5EE9978ED23F /* AuthenticationStartScreenViewModelTests.swift in Sources */,
CEAEA57B7665C8E790599A78 /* BlockedUsersScreenViewModelTests.swift in Sources */, CEAEA57B7665C8E790599A78 /* BlockedUsersScreenViewModelTests.swift in Sources */,
1B2F9F368619FFF8C63C87CC /* BugReportScreenViewModelTests.swift in Sources */, 1B2F9F368619FFF8C63C87CC /* BugReportScreenViewModelTests.swift in Sources */,
@ -6278,6 +6299,9 @@
7BD2123144A32F082CECC108 /* AudioRoomTimelineView.swift in Sources */, 7BD2123144A32F082CECC108 /* AudioRoomTimelineView.swift in Sources */,
9278EC51D24E57445B290521 /* AudioSessionProtocol.swift in Sources */, 9278EC51D24E57445B290521 /* AudioSessionProtocol.swift in Sources */,
8A6CB15C8FC68F557750BF54 /* AuthenticationClientBuilder.swift in Sources */, 8A6CB15C8FC68F557750BF54 /* AuthenticationClientBuilder.swift in Sources */,
210DB40676DF2A23E69C2D06 /* AuthenticationClientBuilderFactory.swift in Sources */,
A51C65E5A3C9F2464A91A380 /* AuthenticationClientBuilderFactoryMock.swift in Sources */,
0F4709282FCCFBEFED427B8A /* AuthenticationClientBuilderMock.swift in Sources */,
67E9926C4572C54F59FCA91A /* AuthenticationFlowCoordinator.swift in Sources */, 67E9926C4572C54F59FCA91A /* AuthenticationFlowCoordinator.swift in Sources */,
9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */, 9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */,
56DACDD379A86A1F5DEFE7BE /* AuthenticationServiceProtocol.swift in Sources */, 56DACDD379A86A1F5DEFE7BE /* AuthenticationServiceProtocol.swift in Sources */,
@ -6333,6 +6357,7 @@
1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */, 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */,
DDFBDEE1DC32BDD5488F898C /* ClientProxyMock.swift in Sources */, DDFBDEE1DC32BDD5488F898C /* ClientProxyMock.swift in Sources */,
24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */, 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */,
41C5DA0C06F30311A221E85B /* ClientSDKMock.swift in Sources */,
0C797CD650DFD2876BEC5173 /* CollapsibleReactionLayout.swift in Sources */, 0C797CD650DFD2876BEC5173 /* CollapsibleReactionLayout.swift in Sources */,
78A3D84BA47DAC69B4D0A34C /* CollapsibleRoomTimelineView.swift in Sources */, 78A3D84BA47DAC69B4D0A34C /* CollapsibleRoomTimelineView.swift in Sources */,
0DC815CA24E1BD7F408F37D3 /* CollapsibleTimelineItem.swift in Sources */, 0DC815CA24E1BD7F408F37D3 /* CollapsibleTimelineItem.swift in Sources */,
@ -6423,7 +6448,7 @@
50539366B408780B232C1910 /* EstimatedWaveformView.swift in Sources */, 50539366B408780B232C1910 /* EstimatedWaveformView.swift in Sources */,
F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */, F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */,
02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */,
EE57A96130DD8DB053790AE2 /* EventTimelineItemSDKMock.swift in Sources */, 5BBDF9926CB645DE2F7BC258 /* EventTimelineItemSDKMock.swift in Sources */,
63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */, 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */,
5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */, 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */,
D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */, D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */,
@ -6562,7 +6587,6 @@
F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */, F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */,
C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */, C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */,
C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */, C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */,
32FC143630CE22A9E403370B /* MockAuthenticationService.swift in Sources */,
B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */, B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */,
09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */, 09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */,
B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */, B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */,
@ -6982,6 +7006,7 @@
6586E1F1D5F0651D0638FFAF /* UserSessionMock.swift in Sources */, 6586E1F1D5F0651D0638FFAF /* UserSessionMock.swift in Sources */,
978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */, 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */,
7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */, 7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */,
79D57E9AE03A2DC689D14EA2 /* UserSessionStoreMock.swift in Sources */,
AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */, AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */,
F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */, F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */,
1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */, 1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */,
@ -7026,7 +7051,6 @@
7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */, 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */,
94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */, 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */,
9F19096BFA629C0AC282B1E4 /* CreateRoomScreenUITests.swift in Sources */, 9F19096BFA629C0AC282B1E4 /* CreateRoomScreenUITests.swift in Sources */,
5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */,
0CF81807BE5FBFC9E2BBCECF /* PollFormScreenUITests.swift in Sources */, 0CF81807BE5FBFC9E2BBCECF /* PollFormScreenUITests.swift in Sources */,
44121202B4A260C98BF615A7 /* RoomMembersListScreenUITests.swift in Sources */, 44121202B4A260C98BF615A7 /* RoomMembersListScreenUITests.swift in Sources */,
D29E046C1E3045E0346C479D /* RoomRolesAndPermissionsUITests.swift in Sources */, D29E046C1E3045E0346C479D /* RoomRolesAndPermissionsUITests.swift in Sources */,

View File

@ -485,7 +485,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
encryptionKeyProvider: EncryptionKeyProvider(), encryptionKeyProvider: EncryptionKeyProvider(),
appSettings: appSettings, appSettings: appSettings,
appHooks: appHooks) appHooks: appHooks)
_ = await authenticationService.configure(for: userSession.clientProxy.homeserver) _ = await authenticationService.configure(for: userSession.clientProxy.homeserver, flow: .login)
let parameters = SoftLogoutScreenCoordinatorParameters(authenticationService: authenticationService, let parameters = SoftLogoutScreenCoordinatorParameters(authenticationService: authenticationService,
credentials: credentials, credentials: credentials,

View File

@ -86,11 +86,11 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .loginManually: case .loginManually:
Task { await self.startAuthentication(flow: .login) } showServerConfirmationScreen(authenticationFlow: .login)
case .loginWithQR: case .loginWithQR:
startQRCodeLogin() startQRCodeLogin()
case .register: case .register:
Task { await self.startAuthentication(flow: .register) } showServerConfirmationScreen(authenticationFlow: .register)
case .reportProblem: case .reportProblem:
showReportProblemScreen() showReportProblemScreen()
} }
@ -113,7 +113,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .signInManually: case .signInManually:
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator.setSheetCoordinator(nil)
Task { await self.startAuthentication(flow: .login) } showServerConfirmationScreen(authenticationFlow: .login)
case .cancel: case .cancel:
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator.setSheetCoordinator(nil)
case .done(let userSession): case .done(let userSession):
@ -137,25 +137,14 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
bugReportFlowCoordinator?.start() bugReportFlowCoordinator?.start()
} }
private func startAuthentication(flow: AuthenticationFlow) async { // TODO: Move this method after showServerConfirmationScreen
startLoading() private func showServerSelectionScreen(authenticationFlow: AuthenticationFlow) {
switch await authenticationService.configure(for: appSettings.defaultHomeserverAddress) {
case .success:
stopLoading()
showServerConfirmationScreen(authenticationFlow: flow)
case .failure:
stopLoading()
showServerSelectionScreen(authenticationFlow: flow, isModallyPresented: false)
}
}
private func showServerSelectionScreen(authenticationFlow: AuthenticationFlow, isModallyPresented: Bool) {
let navigationCoordinator = NavigationStackCoordinator() let navigationCoordinator = NavigationStackCoordinator()
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService, let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
userIndicatorController: userIndicatorController, authenticationFlow: authenticationFlow,
isModallyPresented: isModallyPresented) slidingSyncLearnMoreURL: appSettings.slidingSyncLearnMoreURL,
userIndicatorController: userIndicatorController)
let coordinator = ServerSelectionScreenCoordinator(parameters: parameters) let coordinator = ServerSelectionScreenCoordinator(parameters: parameters)
coordinator.actions coordinator.actions
@ -164,42 +153,26 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
switch action { switch action {
case .updated: case .updated:
if isModallyPresented { navigationStackCoordinator.setSheetCoordinator(nil)
navigationStackCoordinator.setSheetCoordinator(nil)
} else {
// We are here because the default server failed to respond.
if authenticationService.homeserver.value.loginMode == .password {
if authenticationFlow == .login {
// Add the password login screen directly to the flow, its fine.
showLoginScreen()
} else {
// Add the web registration screen directly to the flow, its fine.
showWebRegistration()
}
} else {
// OIDC is presented from the confirmation screen so replace the
// server selection screen which was inserted to handle the failure.
navigationStackCoordinator.pop()
showServerConfirmationScreen(authenticationFlow: authenticationFlow)
}
}
case .dismiss: case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator.setSheetCoordinator(nil)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
if isModallyPresented { navigationCoordinator.setRootCoordinator(coordinator)
navigationCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setSheetCoordinator(navigationCoordinator)
navigationStackCoordinator.setSheetCoordinator(navigationCoordinator)
} else {
navigationStackCoordinator.push(coordinator)
}
} }
private func showServerConfirmationScreen(authenticationFlow: AuthenticationFlow) { private func showServerConfirmationScreen(authenticationFlow: AuthenticationFlow) {
// Reset the service back to the default homeserver before continuing. This ensures
// we check that registration is supported if it was previously configured for login.
authenticationService.reset()
let parameters = ServerConfirmationScreenCoordinatorParameters(authenticationService: authenticationService, let parameters = ServerConfirmationScreenCoordinatorParameters(authenticationService: authenticationService,
authenticationFlow: authenticationFlow) authenticationFlow: authenticationFlow,
slidingSyncLearnMoreURL: appSettings.slidingSyncLearnMoreURL,
userIndicatorController: userIndicatorController)
let coordinator = ServerConfirmationScreenCoordinator(parameters: parameters) let coordinator = ServerConfirmationScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in coordinator.actions.sink { [weak self] action in
@ -215,7 +188,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
showLoginScreen() showLoginScreen()
} }
case .changeServer: case .changeServer:
showServerSelectionScreen(authenticationFlow: authenticationFlow, isModallyPresented: true) showServerSelectionScreen(authenticationFlow: authenticationFlow)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)

View File

@ -0,0 +1,22 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
import MatrixRustSDK
extension AuthenticationClientBuilderFactoryMock {
struct Configuration {
var builderConfiguration: AuthenticationClientBuilderMock.Configuration = .init()
}
convenience init(configuration: Configuration) {
self.init()
let clientBuilder = AuthenticationClientBuilderMock(configuration: configuration.builderConfiguration)
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue = clientBuilder
}
}

View File

@ -0,0 +1,47 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
import MatrixRustSDK
extension AuthenticationClientBuilderMock {
struct Configuration {
var homeserverClients = [
"matrix.org": ClientSDKMock(configuration: .init()),
"example.com": ClientSDKMock(configuration: .init(serverAddress: "example.com",
homeserverURL: "https://matrix.example.com",
slidingSyncVersion: .native,
supportsPasswordLogin: true,
elementWellKnown: "")),
"company.com": ClientSDKMock(configuration: .init(serverAddress: "company.com",
homeserverURL: "https://matrix.company.com",
slidingSyncVersion: .native,
oidcLoginURL: "https://auth.company.com/oidc",
supportsPasswordLogin: false,
elementWellKnown: "")),
"server.net": ClientSDKMock(configuration: .init(serverAddress: "server.net",
homeserverURL: "https://matrix.example.com",
slidingSyncVersion: .native,
supportsPasswordLogin: false,
elementWellKnown: ""))
]
var qrCodeClient = ClientSDKMock(configuration: .init())
}
convenience init(configuration: Configuration) {
self.init()
buildHomeserverAddressClosure = { address in
guard let client = configuration.homeserverClients[address] else {
throw ClientBuildError.ServerUnreachable(message: "Not a known homeserver.")
}
return client
}
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue = configuration.qrCodeClient
}
}

View File

@ -1819,6 +1819,230 @@ class AudioSessionMock: AudioSessionProtocol {
try setActiveOptionsClosure?(active, options) try setActiveOptionsClosure?(active, options)
} }
} }
class AuthenticationClientBuilderFactoryMock: AuthenticationClientBuilderFactoryProtocol {
//MARK: - makeBuilder
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = 0
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount: Int {
get {
if Thread.isMainThread {
return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue
}
}
}
}
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCalled: Bool {
return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount > 0
}
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments: (sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)?
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocations: [(sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)] = []
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue: AuthenticationClientBuilderProtocol!
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue: AuthenticationClientBuilderProtocol! {
get {
if Thread.isMainThread {
return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue
} else {
var returnValue: AuthenticationClientBuilderProtocol? = nil
DispatchQueue.main.sync {
returnValue = makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue
}
}
}
}
var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure: ((SessionDirectories, String, ClientSessionDelegate, AppSettings, AppHooks) -> AuthenticationClientBuilderProtocol)?
func makeBuilder(sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks) -> AuthenticationClientBuilderProtocol {
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount += 1
makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments = (sessionDirectories: sessionDirectories, passphrase: passphrase, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks)
DispatchQueue.main.async {
self.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocations.append((sessionDirectories: sessionDirectories, passphrase: passphrase, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks))
}
if let makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure = makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure {
return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure(sessionDirectories, passphrase, clientSessionDelegate, appSettings, appHooks)
} else {
return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue
}
}
}
class AuthenticationClientBuilderMock: AuthenticationClientBuilderProtocol {
//MARK: - build
var buildHomeserverAddressThrowableError: Error?
var buildHomeserverAddressUnderlyingCallsCount = 0
var buildHomeserverAddressCallsCount: Int {
get {
if Thread.isMainThread {
return buildHomeserverAddressUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = buildHomeserverAddressUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
buildHomeserverAddressUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
buildHomeserverAddressUnderlyingCallsCount = newValue
}
}
}
}
var buildHomeserverAddressCalled: Bool {
return buildHomeserverAddressCallsCount > 0
}
var buildHomeserverAddressReceivedHomeserverAddress: String?
var buildHomeserverAddressReceivedInvocations: [String] = []
var buildHomeserverAddressUnderlyingReturnValue: ClientProtocol!
var buildHomeserverAddressReturnValue: ClientProtocol! {
get {
if Thread.isMainThread {
return buildHomeserverAddressUnderlyingReturnValue
} else {
var returnValue: ClientProtocol? = nil
DispatchQueue.main.sync {
returnValue = buildHomeserverAddressUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
buildHomeserverAddressUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
buildHomeserverAddressUnderlyingReturnValue = newValue
}
}
}
}
var buildHomeserverAddressClosure: ((String) async throws -> ClientProtocol)?
func build(homeserverAddress: String) async throws -> ClientProtocol {
if let error = buildHomeserverAddressThrowableError {
throw error
}
buildHomeserverAddressCallsCount += 1
buildHomeserverAddressReceivedHomeserverAddress = homeserverAddress
DispatchQueue.main.async {
self.buildHomeserverAddressReceivedInvocations.append(homeserverAddress)
}
if let buildHomeserverAddressClosure = buildHomeserverAddressClosure {
return try await buildHomeserverAddressClosure(homeserverAddress)
} else {
return buildHomeserverAddressReturnValue
}
}
//MARK: - buildWithQRCode
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerThrowableError: Error?
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount = 0
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCallsCount: Int {
get {
if Thread.isMainThread {
return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount = newValue
}
}
}
}
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCalled: Bool {
return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCallsCount > 0
}
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedArguments: (qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, progressListener: QrLoginProgressListenerProxy)?
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedInvocations: [(qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, progressListener: QrLoginProgressListenerProxy)] = []
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue: ClientProtocol!
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue: ClientProtocol! {
get {
if Thread.isMainThread {
return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue
} else {
var returnValue: ClientProtocol? = nil
DispatchQueue.main.sync {
returnValue = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue = newValue
}
}
}
}
var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure: ((QrCodeData, OIDCConfigurationProxy, QrLoginProgressListenerProxy) async throws -> ClientProtocol)?
func buildWithQRCode(qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, progressListener: QrLoginProgressListenerProxy) async throws -> ClientProtocol {
if let error = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerThrowableError {
throw error
}
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCallsCount += 1
buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedArguments = (qrCodeData: qrCodeData, oidcConfiguration: oidcConfiguration, progressListener: progressListener)
DispatchQueue.main.async {
self.buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedInvocations.append((qrCodeData: qrCodeData, oidcConfiguration: oidcConfiguration, progressListener: progressListener))
}
if let buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure {
return try await buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure(qrCodeData, oidcConfiguration, progressListener)
} else {
return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue
}
}
}
class BugReportServiceMock: BugReportServiceProtocol { class BugReportServiceMock: BugReportServiceProtocol {
var crashedLastRun: Bool { var crashedLastRun: Bool {
get { return underlyingCrashedLastRun } get { return underlyingCrashedLastRun }
@ -15296,6 +15520,271 @@ class UserSessionMock: UserSessionProtocol {
var underlyingCallbacks: PassthroughSubject<UserSessionCallback, Never>! var underlyingCallbacks: PassthroughSubject<UserSessionCallback, Never>!
} }
class UserSessionStoreMock: UserSessionStoreProtocol {
var hasSessions: Bool {
get { return underlyingHasSessions }
set(value) { underlyingHasSessions = value }
}
var underlyingHasSessions: Bool!
var userIDs: [String] = []
var clientSessionDelegate: ClientSessionDelegate {
get { return underlyingClientSessionDelegate }
set(value) { underlyingClientSessionDelegate = value }
}
var underlyingClientSessionDelegate: ClientSessionDelegate!
//MARK: - reset
var resetUnderlyingCallsCount = 0
var resetCallsCount: Int {
get {
if Thread.isMainThread {
return resetUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = resetUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
resetUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
resetUnderlyingCallsCount = newValue
}
}
}
}
var resetCalled: Bool {
return resetCallsCount > 0
}
var resetClosure: (() -> Void)?
func reset() {
resetCallsCount += 1
resetClosure?()
}
//MARK: - restoreUserSession
var restoreUserSessionUnderlyingCallsCount = 0
var restoreUserSessionCallsCount: Int {
get {
if Thread.isMainThread {
return restoreUserSessionUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = restoreUserSessionUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
restoreUserSessionUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
restoreUserSessionUnderlyingCallsCount = newValue
}
}
}
}
var restoreUserSessionCalled: Bool {
return restoreUserSessionCallsCount > 0
}
var restoreUserSessionUnderlyingReturnValue: Result<UserSessionProtocol, UserSessionStoreError>!
var restoreUserSessionReturnValue: Result<UserSessionProtocol, UserSessionStoreError>! {
get {
if Thread.isMainThread {
return restoreUserSessionUnderlyingReturnValue
} else {
var returnValue: Result<UserSessionProtocol, UserSessionStoreError>? = nil
DispatchQueue.main.sync {
returnValue = restoreUserSessionUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
restoreUserSessionUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
restoreUserSessionUnderlyingReturnValue = newValue
}
}
}
}
var restoreUserSessionClosure: (() async -> Result<UserSessionProtocol, UserSessionStoreError>)?
func restoreUserSession() async -> Result<UserSessionProtocol, UserSessionStoreError> {
restoreUserSessionCallsCount += 1
if let restoreUserSessionClosure = restoreUserSessionClosure {
return await restoreUserSessionClosure()
} else {
return restoreUserSessionReturnValue
}
}
//MARK: - userSession
var userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = 0
var userSessionForSessionDirectoriesPassphraseCallsCount: Int {
get {
if Thread.isMainThread {
return userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = newValue
}
}
}
}
var userSessionForSessionDirectoriesPassphraseCalled: Bool {
return userSessionForSessionDirectoriesPassphraseCallsCount > 0
}
var userSessionForSessionDirectoriesPassphraseReceivedArguments: (client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?)?
var userSessionForSessionDirectoriesPassphraseReceivedInvocations: [(client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?)] = []
var userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue: Result<UserSessionProtocol, UserSessionStoreError>!
var userSessionForSessionDirectoriesPassphraseReturnValue: Result<UserSessionProtocol, UserSessionStoreError>! {
get {
if Thread.isMainThread {
return userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue
} else {
var returnValue: Result<UserSessionProtocol, UserSessionStoreError>? = nil
DispatchQueue.main.sync {
returnValue = userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue = newValue
}
}
}
}
var userSessionForSessionDirectoriesPassphraseClosure: ((ClientProtocol, SessionDirectories, String?) async -> Result<UserSessionProtocol, UserSessionStoreError>)?
func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError> {
userSessionForSessionDirectoriesPassphraseCallsCount += 1
userSessionForSessionDirectoriesPassphraseReceivedArguments = (client: client, sessionDirectories: sessionDirectories, passphrase: passphrase)
DispatchQueue.main.async {
self.userSessionForSessionDirectoriesPassphraseReceivedInvocations.append((client: client, sessionDirectories: sessionDirectories, passphrase: passphrase))
}
if let userSessionForSessionDirectoriesPassphraseClosure = userSessionForSessionDirectoriesPassphraseClosure {
return await userSessionForSessionDirectoriesPassphraseClosure(client, sessionDirectories, passphrase)
} else {
return userSessionForSessionDirectoriesPassphraseReturnValue
}
}
//MARK: - logout
var logoutUserSessionUnderlyingCallsCount = 0
var logoutUserSessionCallsCount: Int {
get {
if Thread.isMainThread {
return logoutUserSessionUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = logoutUserSessionUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
logoutUserSessionUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
logoutUserSessionUnderlyingCallsCount = newValue
}
}
}
}
var logoutUserSessionCalled: Bool {
return logoutUserSessionCallsCount > 0
}
var logoutUserSessionReceivedUserSession: UserSessionProtocol?
var logoutUserSessionReceivedInvocations: [UserSessionProtocol] = []
var logoutUserSessionClosure: ((UserSessionProtocol) -> Void)?
func logout(userSession: UserSessionProtocol) {
logoutUserSessionCallsCount += 1
logoutUserSessionReceivedUserSession = userSession
DispatchQueue.main.async {
self.logoutUserSessionReceivedInvocations.append(userSession)
}
logoutUserSessionClosure?(userSession)
}
//MARK: - clearCache
var clearCacheForUnderlyingCallsCount = 0
var clearCacheForCallsCount: Int {
get {
if Thread.isMainThread {
return clearCacheForUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = clearCacheForUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
clearCacheForUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
clearCacheForUnderlyingCallsCount = newValue
}
}
}
}
var clearCacheForCalled: Bool {
return clearCacheForCallsCount > 0
}
var clearCacheForReceivedUserID: String?
var clearCacheForReceivedInvocations: [String] = []
var clearCacheForClosure: ((String) -> Void)?
func clearCache(for userID: String) {
clearCacheForCallsCount += 1
clearCacheForReceivedUserID = userID
DispatchQueue.main.async {
self.clearCacheForReceivedInvocations.append(userID)
}
clearCacheForClosure?(userID)
}
}
class VoiceMessageCacheMock: VoiceMessageCacheProtocol { class VoiceMessageCacheMock: VoiceMessageCacheProtocol {
var urlForRecording: URL { var urlForRecording: URL {
get { return underlyingUrlForRecording } get { return underlyingUrlForRecording }

View File

@ -0,0 +1,75 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
import MatrixRustSDK
extension ClientSDKMock {
struct Configuration {
// MARK: Authentication
var serverAddress = "matrix.org"
var homeserverURL = "https://matrix-client.matrix.org"
var slidingSyncVersion = SlidingSyncVersion.native
var oidcLoginURL: String?
var supportsPasswordLogin = true
var elementWellKnown = "{\"registration_helper_url\":\"https://develop.element.io/#/mobile_register\"}"
var validCredentials = (username: "alice", password: "12345678")
// MARK: Session
var userID: String?
var session = Session(accessToken: UUID().uuidString,
refreshToken: nil,
userId: "@alice:matrix.org",
deviceId: UUID().uuidString,
homeserverUrl: "https://matrix-client.matrix.org",
oidcData: nil,
slidingSyncVersion: .native)
}
enum MockError: Error { case generic }
convenience init(configuration: Configuration) {
self.init()
homeserverLoginDetailsReturnValue = HomeserverLoginDetailsSDKMock(configuration: configuration)
slidingSyncVersionReturnValue = configuration.slidingSyncVersion
userIdServerNameThrowableError = MockError.generic
serverReturnValue = "https://\(configuration.serverAddress)"
getUrlUrlReturnValue = configuration.elementWellKnown
urlForOidcLoginOidcConfigurationReturnValue = OidcAuthorizationDataSDKMock(configuration: configuration)
loginUsernamePasswordInitialDeviceNameDeviceIdClosure = { username, password, _, _ in
guard username == configuration.validCredentials.username,
password == configuration.validCredentials.password else {
throw MockError.generic // use the matrix error
}
}
userIdReturnValue = configuration.userID
sessionReturnValue = configuration.session
}
}
extension HomeserverLoginDetailsSDKMock {
convenience init(configuration: ClientSDKMock.Configuration) {
self.init()
slidingSyncVersionReturnValue = configuration.slidingSyncVersion
supportsPasswordLoginReturnValue = configuration.supportsPasswordLogin
supportsOidcLoginReturnValue = configuration.oidcLoginURL != nil
urlReturnValue = configuration.homeserverURL
}
}
extension OidcAuthorizationDataSDKMock {
convenience init(configuration: ClientSDKMock.Configuration) {
self.init()
loginUrlReturnValue = configuration.oidcLoginURL
}
}

View File

@ -0,0 +1,17 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
extension UserSessionStoreMock {
struct Configuration { }
convenience init(configuration: Configuration) {
self.init()
userSessionForSessionDirectoriesPassphraseReturnValue = .success(UserSessionMock(.init(clientProxy: ClientProxyMock(.init()))))
clientSessionDelegate = KeychainControllerMock()
}
}

View File

@ -25,6 +25,11 @@ struct LoginHomeserver: Equatable {
self.registrationHelperURL = registrationHelperURL self.registrationHelperURL = registrationHelperURL
} }
/// Whether or not the app is able to register on this homeserver.
var supportsRegistration: Bool {
loginMode == .oidc || (address == "matrix.org" && registrationHelperURL != nil)
}
/// Sanitizes a user entered homeserver address with the following rules /// Sanitizes a user entered homeserver address with the following rules
/// - Trim any whitespace. /// - Trim any whitespace.
/// - Lowercase the address. /// - Lowercase the address.

View File

@ -145,7 +145,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
startLoading(isInteractionBlocking: false) startLoading(isInteractionBlocking: false)
Task { Task {
switch await authenticationService.configure(for: homeserverDomain) { switch await authenticationService.configure(for: homeserverDomain, flow: .login) {
case .success: case .success:
stopLoading() stopLoading()
if authenticationService.homeserver.value.loginMode == .oidc { if authenticationService.homeserver.value.loginMode == .oidc {

View File

@ -11,6 +11,8 @@ import SwiftUI
struct ServerConfirmationScreenCoordinatorParameters { struct ServerConfirmationScreenCoordinatorParameters {
let authenticationService: AuthenticationServiceProtocol let authenticationService: AuthenticationServiceProtocol
let authenticationFlow: AuthenticationFlow let authenticationFlow: AuthenticationFlow
let slidingSyncLearnMoreURL: URL
let userIndicatorController: UserIndicatorControllerProtocol
} }
enum ServerConfirmationScreenCoordinatorAction { enum ServerConfirmationScreenCoordinatorAction {
@ -29,7 +31,9 @@ final class ServerConfirmationScreenCoordinator: CoordinatorProtocol {
init(parameters: ServerConfirmationScreenCoordinatorParameters) { init(parameters: ServerConfirmationScreenCoordinatorParameters) {
viewModel = ServerConfirmationScreenViewModel(authenticationService: parameters.authenticationService, viewModel = ServerConfirmationScreenViewModel(authenticationService: parameters.authenticationService,
authenticationFlow: parameters.authenticationFlow) authenticationFlow: parameters.authenticationFlow,
slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL,
userIndicatorController: parameters.userIndicatorController)
} }
func start() { func start() {

View File

@ -19,11 +19,11 @@ struct ServerConfirmationScreenViewState: BindableState {
var homeserverAddress: String var homeserverAddress: String
/// The flow being attempted on the selected homeserver. /// The flow being attempted on the selected homeserver.
let authenticationFlow: AuthenticationFlow let authenticationFlow: AuthenticationFlow
/// Whether or not the homeserver supports registration.
var homeserverSupportsRegistration = false
/// The presentation anchor used for OIDC authentication. /// The presentation anchor used for OIDC authentication.
var window: UIWindow? var window: UIWindow?
var bindings = ServerConfirmationScreenBindings()
/// The screen's title. /// The screen's title.
var title: String { var title: String {
switch authenticationFlow { switch authenticationFlow {
@ -46,23 +46,16 @@ struct ServerConfirmationScreenViewState: BindableState {
"" ""
} }
case .register: case .register:
if canContinue { L10n.screenServerConfirmationMessageRegister
L10n.screenServerConfirmationMessageRegister
} else {
L10n.errorAccountCreationNotPossible
}
}
}
/// Whether or not it is valid to continue the flow.
var canContinue: Bool {
switch authenticationFlow {
case .login: true
case .register: homeserverSupportsRegistration
} }
} }
} }
struct ServerConfirmationScreenBindings {
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<ServerConfirmationScreenAlert>?
}
enum ServerConfirmationScreenViewAction { enum ServerConfirmationScreenViewAction {
/// Updates the window used as the OIDC presentation anchor. /// Updates the window used as the OIDC presentation anchor.
case updateWindow(UIWindow) case updateWindow(UIWindow)
@ -71,3 +64,16 @@ enum ServerConfirmationScreenViewAction {
/// The user would like to change to a different homeserver. /// The user would like to change to a different homeserver.
case changeServer case changeServer
} }
enum ServerConfirmationScreenAlert: Hashable {
/// An alert that informs the user that a server could not be found.
case homeserverNotFound
/// An alert that informs the user about a bad well-known file.
case invalidWellKnown(String)
/// An alert that allows the user to learn about sliding sync.
case slidingSync
/// An alert that informs the user that registration isn't supported.
case registration
/// An unknown error has occurred.
case unknownError
}

View File

@ -11,46 +11,118 @@ import SwiftUI
typealias ServerConfirmationScreenViewModelType = StateStoreViewModel<ServerConfirmationScreenViewState, ServerConfirmationScreenViewAction> typealias ServerConfirmationScreenViewModelType = StateStoreViewModel<ServerConfirmationScreenViewState, ServerConfirmationScreenViewAction>
class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, ServerConfirmationScreenViewModelProtocol { class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, ServerConfirmationScreenViewModelProtocol {
let authenticationService: AuthenticationServiceProtocol
let authenticationFlow: AuthenticationFlow
let slidingSyncLearnMoreURL: URL
let userIndicatorController: UserIndicatorControllerProtocol
private var actionsSubject: PassthroughSubject<ServerConfirmationScreenViewModelAction, Never> = .init() private var actionsSubject: PassthroughSubject<ServerConfirmationScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<ServerConfirmationScreenViewModelAction, Never> { var actions: AnyPublisher<ServerConfirmationScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
init(authenticationService: AuthenticationServiceProtocol, authenticationFlow: AuthenticationFlow) { init(authenticationService: AuthenticationServiceProtocol,
let homeserver = authenticationService.homeserver.value authenticationFlow: AuthenticationFlow,
slidingSyncLearnMoreURL: URL,
userIndicatorController: UserIndicatorControllerProtocol) {
self.authenticationService = authenticationService
self.authenticationFlow = authenticationFlow
self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL
self.userIndicatorController = userIndicatorController
let homeserver = authenticationService.homeserver.value
super.init(initialViewState: ServerConfirmationScreenViewState(homeserverAddress: homeserver.address, super.init(initialViewState: ServerConfirmationScreenViewState(homeserverAddress: homeserver.address,
authenticationFlow: authenticationFlow, authenticationFlow: authenticationFlow))
homeserverSupportsRegistration: homeserver.supportsRegistration))
authenticationService.homeserver authenticationService.homeserver
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] homeserver in .sink { [weak self] homeserver in
guard let self else { return } guard let self else { return }
state.homeserverAddress = homeserver.address state.homeserverAddress = homeserver.address
state.homeserverSupportsRegistration = homeserver.supportsRegistration
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
// MARK: - Public
override func process(viewAction: ServerConfirmationScreenViewAction) { override func process(viewAction: ServerConfirmationScreenViewAction) {
switch viewAction { switch viewAction {
case .updateWindow(let window): case .updateWindow(let window):
guard state.window != window else { return } guard state.window != window else { return }
Task { state.window = window } Task { state.window = window }
case .confirm: case .confirm:
actionsSubject.send(.confirm) Task { await configureAndContinue() }
case .changeServer: case .changeServer:
actionsSubject.send(.changeServer) actionsSubject.send(.changeServer)
} }
} }
}
// MARK: - Private
extension LoginHomeserver {
var supportsRegistration: Bool { private func configureAndContinue() async {
loginMode == .oidc || (address == "matrix.org" && registrationHelperURL != nil) let homeserver = authenticationService.homeserver.value
// If the login mode is unknown, the service hasn't be configured and we need to do it now.
// Otherwise we can continue the flow as server selection has been performed and succeeded.
guard homeserver.loginMode == .unknown || authenticationService.flow != authenticationFlow else {
actionsSubject.send(.confirm)
return
}
startLoading()
defer { stopLoading() }
switch await authenticationService.configure(for: homeserver.address, flow: authenticationFlow) {
case .success:
actionsSubject.send(.confirm)
case .failure(let error):
switch error {
case .invalidServer, .invalidHomeserverAddress:
displayError(.homeserverNotFound)
case .invalidWellKnown(let error):
displayError(.invalidWellKnown(error))
case .slidingSyncNotAvailable:
displayError(.slidingSync)
case .registrationNotSupported:
displayError(.registration)
default:
displayError(.unknownError)
}
}
}
private func startLoading(label: String = L10n.commonLoading) {
userIndicatorController.submitIndicator(UserIndicator(type: .modal,
title: label,
persistent: true))
}
private func stopLoading() {
userIndicatorController.retractAllIndicators()
}
private func displayError(_ type: ServerConfirmationScreenAlert) {
switch type {
case .homeserverNotFound:
state.bindings.alertInfo = AlertInfo(id: .homeserverNotFound,
title: L10n.errorUnknown,
message: L10n.screenChangeServerErrorInvalidHomeserver)
case .invalidWellKnown(let error):
state.bindings.alertInfo = AlertInfo(id: .invalidWellKnown(error),
title: L10n.commonServerNotSupported,
message: L10n.screenChangeServerErrorInvalidWellKnown(error))
case .slidingSync:
let openURL = { UIApplication.shared.open(self.slidingSyncLearnMoreURL) }
state.bindings.alertInfo = AlertInfo(id: .slidingSync,
title: L10n.commonServerNotSupported,
message: L10n.screenChangeServerErrorNoSlidingSyncMessage,
primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL),
secondaryButton: .init(title: L10n.actionCancel, action: nil))
case .registration:
state.bindings.alertInfo = AlertInfo(id: .registration,
title: L10n.errorUnknown,
message: L10n.errorAccountCreationNotPossible)
case .unknownError:
state.bindings.alertInfo = AlertInfo(id: .unknownError)
}
} }
} }

View File

@ -19,6 +19,7 @@ struct ServerConfirmationScreen: View {
} }
.background() .background()
.backgroundStyle(.compound.bgCanvasDefault) .backgroundStyle(.compound.bgCanvasDefault)
.alert(item: $context.alertInfo)
.introspect(.window, on: .supportedVersions) { window in .introspect(.window, on: .supportedVersions) { window in
context.send(viewAction: .updateWindow(window)) context.send(viewAction: .updateWindow(window))
} }
@ -53,7 +54,6 @@ struct ServerConfirmationScreen: View {
} }
.buttonStyle(.compound(.primary)) .buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.serverConfirmationScreen.continue) .accessibilityIdentifier(A11yIdentifiers.serverConfirmationScreen.continue)
.disabled(!context.viewState.canContinue)
Button { context.send(viewAction: .changeServer) } label: { Button { context.send(viewAction: .changeServer) } label: {
Text(L10n.screenServerConfirmationChangeServer) Text(L10n.screenServerConfirmationChangeServer)
@ -68,10 +68,8 @@ struct ServerConfirmationScreen: View {
// MARK: - Previews // MARK: - Previews
struct ServerConfirmationScreen_Previews: PreviewProvider, TestablePreview { struct ServerConfirmationScreen_Previews: PreviewProvider, TestablePreview {
static let loginViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationService(), static let loginViewModel = makeViewModel(flow: .login)
authenticationFlow: .login) static let registerViewModel = makeViewModel(flow: .register)
static let registerViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationService(),
authenticationFlow: .register)
static var previews: some View { static var previews: some View {
NavigationStack { NavigationStack {
@ -86,4 +84,11 @@ struct ServerConfirmationScreen_Previews: PreviewProvider, TestablePreview {
} }
.previewDisplayName("Register") .previewDisplayName("Register")
} }
static func makeViewModel(flow: AuthenticationFlow) -> ServerConfirmationScreenViewModel {
ServerConfirmationScreenViewModel(authenticationService: AuthenticationService.mock,
authenticationFlow: flow,
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
userIndicatorController: UserIndicatorControllerMock())
}
} }

View File

@ -11,30 +11,22 @@ enum MockServerSelectionScreenState: CaseIterable {
case matrix case matrix
case emptyAddress case emptyAddress
case invalidAddress case invalidAddress
case nonModal
/// Generate the view struct for the screen state. /// Generate the view struct for the screen state.
@MainActor var viewModel: ServerSelectionScreenViewModel { @MainActor var viewModel: ServerSelectionScreenViewModel {
switch self { switch self {
case .matrix: case .matrix:
return ServerSelectionScreenViewModel(homeserverAddress: "https://matrix.org", return ServerSelectionScreenViewModel(homeserverAddress: "https://matrix.org",
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
isModallyPresented: true)
case .emptyAddress: case .emptyAddress:
return ServerSelectionScreenViewModel(homeserverAddress: "", return ServerSelectionScreenViewModel(homeserverAddress: "",
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
isModallyPresented: true)
case .invalidAddress: case .invalidAddress:
let viewModel = ServerSelectionScreenViewModel(homeserverAddress: "thisisbad", let viewModel = ServerSelectionScreenViewModel(homeserverAddress: "thisisbad",
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
isModallyPresented: true)
viewModel.displayError(.footerMessage(L10n.errorUnknown)) viewModel.displayError(.footerMessage(L10n.errorUnknown))
return viewModel return viewModel
case .nonModal:
return ServerSelectionScreenViewModel(homeserverAddress: "https://matrix.org",
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
isModallyPresented: false)
} }
} }
} }

View File

@ -11,9 +11,9 @@ import SwiftUI
struct ServerSelectionScreenCoordinatorParameters { struct ServerSelectionScreenCoordinatorParameters {
/// The service used to authenticate the user. /// The service used to authenticate the user.
let authenticationService: AuthenticationServiceProtocol let authenticationService: AuthenticationServiceProtocol
let authenticationFlow: AuthenticationFlow
let slidingSyncLearnMoreURL: URL
let userIndicatorController: UserIndicatorControllerProtocol let userIndicatorController: UserIndicatorControllerProtocol
/// Whether the screen is presented modally or within a navigation stack.
let isModallyPresented: Bool
} }
enum ServerSelectionScreenCoordinatorAction { enum ServerSelectionScreenCoordinatorAction {
@ -38,8 +38,7 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
init(parameters: ServerSelectionScreenCoordinatorParameters) { init(parameters: ServerSelectionScreenCoordinatorParameters) {
self.parameters = parameters self.parameters = parameters
viewModel = ServerSelectionScreenViewModel(homeserverAddress: parameters.authenticationService.homeserver.value.address, viewModel = ServerSelectionScreenViewModel(homeserverAddress: parameters.authenticationService.homeserver.value.address,
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL)
isModallyPresented: parameters.isModallyPresented)
userIndicatorController = parameters.userIndicatorController userIndicatorController = parameters.userIndicatorController
} }
@ -85,7 +84,7 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
startLoading() startLoading()
Task { Task {
switch await authenticationService.configure(for: homeserverAddress) { switch await authenticationService.configure(for: homeserverAddress, flow: parameters.authenticationFlow) {
case .success: case .success:
MXLog.info("Selected homeserver: \(homeserverAddress)") MXLog.info("Selected homeserver: \(homeserverAddress)")
actionsSubject.send(.updated) actionsSubject.send(.updated)
@ -107,6 +106,8 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
viewModel.displayError(.invalidWellKnownAlert(error)) viewModel.displayError(.invalidWellKnownAlert(error))
case .slidingSyncNotAvailable: case .slidingSyncNotAvailable:
viewModel.displayError(.slidingSyncAlert) viewModel.displayError(.slidingSyncAlert)
case .registrationNotSupported:
viewModel.displayError(.registrationAlert) // TODO: [DOUG] Test me!
default: default:
viewModel.displayError(.footerMessage(L10n.errorUnknown)) viewModel.displayError(.footerMessage(L10n.errorUnknown))
} }

View File

@ -22,19 +22,12 @@ struct ServerSelectionScreenViewState: BindableState {
var bindings: ServerSelectionScreenBindings var bindings: ServerSelectionScreenBindings
/// An error message to be shown in the text field footer. /// An error message to be shown in the text field footer.
var footerErrorMessage: String? var footerErrorMessage: String?
/// Whether the screen is presented modally or within a navigation stack.
var isModallyPresented: Bool
/// The message to show in the text field footer. /// The message to show in the text field footer.
var footerMessage: AttributedString { var footerMessage: AttributedString {
footerErrorMessage.map(AttributedString.init) ?? regularFooterMessage footerErrorMessage.map(AttributedString.init) ?? regularFooterMessage
} }
/// The title shown on the confirm button.
var buttonTitle: String {
isModallyPresented ? L10n.actionContinue : L10n.actionNext
}
/// The text field is showing an error. /// The text field is showing an error.
var isShowingFooterError: Bool { var isShowingFooterError: Bool {
footerErrorMessage != nil footerErrorMessage != nil
@ -45,10 +38,9 @@ struct ServerSelectionScreenViewState: BindableState {
bindings.homeserverAddress.isEmpty || isShowingFooterError bindings.homeserverAddress.isEmpty || isShowingFooterError
} }
init(slidingSyncLearnMoreURL: URL, bindings: ServerSelectionScreenBindings, footerErrorMessage: String? = nil, isModallyPresented: Bool) { init(slidingSyncLearnMoreURL: URL, bindings: ServerSelectionScreenBindings, footerErrorMessage: String? = nil) {
self.bindings = bindings self.bindings = bindings
self.footerErrorMessage = footerErrorMessage self.footerErrorMessage = footerErrorMessage
self.isModallyPresented = isModallyPresented
let linkPlaceholder = "{link}" let linkPlaceholder = "{link}"
var message = AttributedString(L10n.screenChangeServerFormNotice(linkPlaceholder)) var message = AttributedString(L10n.screenChangeServerFormNotice(linkPlaceholder))
@ -82,4 +74,6 @@ enum ServerSelectionScreenErrorType: Hashable {
case invalidWellKnownAlert(String) case invalidWellKnownAlert(String)
/// An alert that allows the user to learn about sliding sync. /// An alert that allows the user to learn about sliding sync.
case slidingSyncAlert case slidingSyncAlert
/// An alert that informs the user that registration isn't supported.
case registrationAlert
} }

View File

@ -19,13 +19,12 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
init(homeserverAddress: String, slidingSyncLearnMoreURL: URL, isModallyPresented: Bool) { init(homeserverAddress: String, slidingSyncLearnMoreURL: URL) {
self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL
let bindings = ServerSelectionScreenBindings(homeserverAddress: homeserverAddress) let bindings = ServerSelectionScreenBindings(homeserverAddress: homeserverAddress)
super.init(initialViewState: ServerSelectionScreenViewState(slidingSyncLearnMoreURL: slidingSyncLearnMoreURL, super.init(initialViewState: ServerSelectionScreenViewState(slidingSyncLearnMoreURL: slidingSyncLearnMoreURL,
bindings: bindings, bindings: bindings))
isModallyPresented: isModallyPresented))
} }
override func process(viewAction: ServerSelectionScreenViewAction) { override func process(viewAction: ServerSelectionScreenViewAction) {
@ -46,7 +45,7 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
state.footerErrorMessage = message state.footerErrorMessage = message
} }
case .invalidWellKnownAlert(let error): case .invalidWellKnownAlert(let error):
state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert, state.bindings.alertInfo = AlertInfo(id: .invalidWellKnownAlert(error),
title: L10n.commonServerNotSupported, title: L10n.commonServerNotSupported,
message: L10n.screenChangeServerErrorInvalidWellKnown(error)) message: L10n.screenChangeServerErrorInvalidWellKnown(error))
case .slidingSyncAlert: case .slidingSyncAlert:
@ -56,6 +55,10 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server
message: L10n.screenChangeServerErrorNoSlidingSyncMessage, message: L10n.screenChangeServerErrorNoSlidingSyncMessage,
primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL), primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL),
secondaryButton: .init(title: L10n.actionCancel, action: nil)) secondaryButton: .init(title: L10n.actionCancel, action: nil))
case .registrationAlert:
state.bindings.alertInfo = AlertInfo(id: .registrationAlert,
title: L10n.errorUnknown,
message: L10n.errorAccountCreationNotPossible)
} }
} }

View File

@ -64,7 +64,7 @@ struct ServerSelectionScreen: View {
.onSubmit(submit) .onSubmit(submit)
Button(action: submit) { Button(action: submit) {
Text(context.viewState.buttonTitle) Text(L10n.actionContinue)
} }
.buttonStyle(.compound(.primary)) .buttonStyle(.compound(.primary))
.disabled(context.viewState.hasValidationError) .disabled(context.viewState.hasValidationError)
@ -72,15 +72,12 @@ struct ServerSelectionScreen: View {
} }
} }
@ToolbarContentBuilder
var toolbar: some ToolbarContent { var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
if context.viewState.isModallyPresented { Button { context.send(viewAction: .dismiss) } label: {
Button { context.send(viewAction: .dismiss) } label: { Text(L10n.actionCancel)
Text(L10n.actionCancel)
}
.accessibilityIdentifier(A11yIdentifiers.changeServerScreen.dismiss)
} }
.accessibilityIdentifier(A11yIdentifiers.changeServerScreen.dismiss)
} }
} }

View File

@ -8,8 +8,16 @@
import Foundation import Foundation
import MatrixRustSDK import MatrixRustSDK
// sourcery: AutoMockable
protocol AuthenticationClientBuilderProtocol {
func build(homeserverAddress: String) async throws -> ClientProtocol
func buildWithQRCode(qrCodeData: QrCodeData,
oidcConfiguration: OIDCConfigurationProxy,
progressListener: QrLoginProgressListenerProxy) async throws -> ClientProtocol
}
/// A wrapper around `ClientBuilder` to share reusable code between Normal and QR logins. /// A wrapper around `ClientBuilder` to share reusable code between Normal and QR logins.
struct AuthenticationClientBuilder { struct AuthenticationClientBuilder: AuthenticationClientBuilderProtocol {
let sessionDirectories: SessionDirectories let sessionDirectories: SessionDirectories
let passphrase: String let passphrase: String
let clientSessionDelegate: ClientSessionDelegate let clientSessionDelegate: ClientSessionDelegate
@ -18,7 +26,7 @@ struct AuthenticationClientBuilder {
let appHooks: AppHooks let appHooks: AppHooks
/// Builds a Client for login using OIDC or password authentication. /// Builds a Client for login using OIDC or password authentication.
func build(homeserverAddress: String) async throws -> Client { func build(homeserverAddress: String) async throws -> ClientProtocol {
if appSettings.slidingSyncDiscovery == .forceNative { if appSettings.slidingSyncDiscovery == .forceNative {
return try await makeClientBuilder(slidingSync: .forceNative).serverNameOrHomeserverUrl(serverNameOrUrl: homeserverAddress).build() return try await makeClientBuilder(slidingSync: .forceNative).serverNameOrHomeserverUrl(serverNameOrUrl: homeserverAddress).build()
} }
@ -38,7 +46,7 @@ struct AuthenticationClientBuilder {
/// Builds a Client, authenticating with the given QR code data. /// Builds a Client, authenticating with the given QR code data.
func buildWithQRCode(qrCodeData: QrCodeData, func buildWithQRCode(qrCodeData: QrCodeData,
oidcConfiguration: OIDCConfigurationProxy, oidcConfiguration: OIDCConfigurationProxy,
progressListener: QrLoginProgressListenerProxy) async throws -> Client { progressListener: QrLoginProgressListenerProxy) async throws -> ClientProtocol {
if appSettings.slidingSyncDiscovery == .forceNative { if appSettings.slidingSyncDiscovery == .forceNative {
return try await makeClientBuilder(slidingSync: .forceNative).buildWithQrCode(qrCodeData: qrCodeData, return try await makeClientBuilder(slidingSync: .forceNative).buildWithQrCode(qrCodeData: qrCodeData,
oidcConfiguration: oidcConfiguration.rustValue, oidcConfiguration: oidcConfiguration.rustValue,

View File

@ -0,0 +1,33 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
import MatrixRustSDK
// sourcery: AutoMockable
protocol AuthenticationClientBuilderFactoryProtocol {
func makeBuilder(sessionDirectories: SessionDirectories,
passphrase: String,
clientSessionDelegate: ClientSessionDelegate,
appSettings: AppSettings,
appHooks: AppHooks) -> AuthenticationClientBuilderProtocol
}
/// A wrapper around `ClientBuilder` to share reusable code between Normal and QR logins.
struct AuthenticationClientBuilderFactory: AuthenticationClientBuilderFactoryProtocol {
func makeBuilder(sessionDirectories: SessionDirectories,
passphrase: String,
clientSessionDelegate: ClientSessionDelegate,
appSettings: AppSettings,
appHooks: AppHooks) -> AuthenticationClientBuilderProtocol {
AuthenticationClientBuilder(sessionDirectories: sessionDirectories,
passphrase: passphrase,
clientSessionDelegate: clientSessionDelegate,
appSettings: appSettings,
appHooks: appHooks)
}
}

View File

@ -10,31 +10,39 @@ import Foundation
import MatrixRustSDK import MatrixRustSDK
class AuthenticationService: AuthenticationServiceProtocol { class AuthenticationService: AuthenticationServiceProtocol {
private var client: Client? private var client: ClientProtocol?
private var sessionDirectories: SessionDirectories private var sessionDirectories: SessionDirectories
private let passphrase: String private let passphrase: String
private let clientBuilderFactory: AuthenticationClientBuilderFactoryProtocol
private let userSessionStore: UserSessionStoreProtocol private let userSessionStore: UserSessionStoreProtocol
private let appSettings: AppSettings private let appSettings: AppSettings
private let appHooks: AppHooks private let appHooks: AppHooks
private let homeserverSubject: CurrentValueSubject<LoginHomeserver, Never> private let homeserverSubject: CurrentValueSubject<LoginHomeserver, Never>
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { homeserverSubject.asCurrentValuePublisher() } var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { homeserverSubject.asCurrentValuePublisher() }
private(set) var flow: AuthenticationFlow
init(userSessionStore: UserSessionStoreProtocol, encryptionKeyProvider: EncryptionKeyProviderProtocol, appSettings: AppSettings, appHooks: AppHooks) { init(userSessionStore: UserSessionStoreProtocol,
encryptionKeyProvider: EncryptionKeyProviderProtocol,
clientBuilderFactory: AuthenticationClientBuilderFactoryProtocol = AuthenticationClientBuilderFactory(),
appSettings: AppSettings,
appHooks: AppHooks) {
sessionDirectories = .init() sessionDirectories = .init()
passphrase = encryptionKeyProvider.generateKey().base64EncodedString() passphrase = encryptionKeyProvider.generateKey().base64EncodedString()
self.clientBuilderFactory = clientBuilderFactory
self.userSessionStore = userSessionStore self.userSessionStore = userSessionStore
self.appSettings = appSettings self.appSettings = appSettings
self.appHooks = appHooks self.appHooks = appHooks
homeserverSubject = .init(LoginHomeserver(address: appSettings.defaultHomeserverAddress, // When updating these, don't forget to update the reset method too.
loginMode: .unknown)) homeserverSubject = .init(LoginHomeserver(address: appSettings.defaultHomeserverAddress, loginMode: .unknown))
flow = .login
} }
// MARK: - Public // MARK: - Public
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> { func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError> {
do { do {
var homeserver = LoginHomeserver(address: homeserverAddress, loginMode: .unknown) var homeserver = LoginHomeserver(address: homeserverAddress, loginMode: .unknown)
@ -57,7 +65,12 @@ class AuthenticationService: AuthenticationServiceProtocol {
case .failure: nil case .failure: nil
} }
if flow == .register, !homeserver.supportsRegistration {
return .failure(.registrationNotSupported)
}
self.client = client self.client = client
self.flow = flow
homeserverSubject.send(homeserver) homeserverSubject.send(homeserver)
return .success(()) return .success(())
} catch ClientBuildError.WellKnownDeserializationError(let error) { } catch ClientBuildError.WellKnownDeserializationError(let error) {
@ -150,18 +163,24 @@ class AuthenticationService: AuthenticationServiceProtocol {
} }
} }
func reset() {
homeserverSubject.send(LoginHomeserver(address: appSettings.defaultHomeserverAddress, loginMode: .unknown))
flow = .login
client = nil
}
// MARK: - Private // MARK: - Private
private func makeClientBuilder() -> AuthenticationClientBuilder { private func makeClientBuilder() -> AuthenticationClientBuilderProtocol {
// Use a fresh session directory each time the user enters a different server // Use a fresh session directory each time the user enters a different server
// so that caches (e.g. server versions) are always fresh for the new server. // so that caches (e.g. server versions) are always fresh for the new server.
rotateSessionDirectory() rotateSessionDirectory()
return AuthenticationClientBuilder(sessionDirectories: sessionDirectories, return clientBuilderFactory.makeBuilder(sessionDirectories: sessionDirectories,
passphrase: passphrase, passphrase: passphrase,
clientSessionDelegate: userSessionStore.clientSessionDelegate, clientSessionDelegate: userSessionStore.clientSessionDelegate,
appSettings: appSettings, appSettings: appSettings,
appHooks: appHooks) appHooks: appHooks)
} }
private func rotateSessionDirectory() { private func rotateSessionDirectory() {
@ -169,7 +188,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
sessionDirectories = .init() sessionDirectories = .init()
} }
private func userSession(for client: Client) async -> Result<UserSessionProtocol, AuthenticationServiceError> { private func userSession(for client: ClientProtocol) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) { switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) {
case .success(let clientProxy): case .success(let clientProxy):
return .success(clientProxy) return .success(clientProxy)
@ -178,3 +197,13 @@ class AuthenticationService: AuthenticationServiceProtocol {
} }
} }
} }
// MARK: - Mocks
extension AuthenticationService {
static var mock = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
encryptionKeyProvider: EncryptionKeyProvider(),
clientBuilderFactory: AuthenticationClientBuilderFactoryMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks())
}

View File

@ -16,7 +16,7 @@ enum AuthenticationFlow {
case register case register
} }
enum AuthenticationServiceError: Error { enum AuthenticationServiceError: Error, Equatable {
/// An error occurred during OIDC authentication. /// An error occurred during OIDC authentication.
case oidcError(OIDCError) case oidcError(OIDCError)
case invalidServer case invalidServer
@ -24,6 +24,7 @@ enum AuthenticationServiceError: Error {
case invalidHomeserverAddress case invalidHomeserverAddress
case invalidWellKnown(String) case invalidWellKnown(String)
case slidingSyncNotAvailable case slidingSyncNotAvailable
case registrationNotSupported
case accountDeactivated case accountDeactivated
case failedLoggingIn case failedLoggingIn
case sessionTokenRefreshNotSupported case sessionTokenRefreshNotSupported
@ -33,9 +34,11 @@ enum AuthenticationServiceError: Error {
protocol AuthenticationServiceProtocol { protocol AuthenticationServiceProtocol {
/// The currently configured homeserver. /// The currently configured homeserver.
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { get } var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { get }
/// The type of flow the service is currently configured with.
var flow: AuthenticationFlow { get }
/// Sets up the service for login on the specified homeserver address. /// Sets up the service for login on the specified homeserver address.
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError>
/// Performs login using OIDC for the current homeserver. /// Performs login using OIDC for the current homeserver.
func urlForOIDCLogin() async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError> func urlForOIDCLogin() async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError>
/// Asks the SDK to abort an ongoing OIDC login if we didn't get a callback to complete the request with. /// Asks the SDK to abort an ongoing OIDC login if we didn't get a callback to complete the request with.
@ -46,6 +49,9 @@ protocol AuthenticationServiceProtocol {
func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError> func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError>
/// Completes registration using the credentials obtained via the helper URL. /// Completes registration using the credentials obtained via the helper URL.
func completeWebRegistration(using credentials: WebRegistrationCredentials) async -> Result<UserSessionProtocol, AuthenticationServiceError> func completeWebRegistration(using credentials: WebRegistrationCredentials) async -> Result<UserSessionProtocol, AuthenticationServiceError>
/// Resets the current configuration requiring `configure(for:flow:)` to be called again.
func reset()
} }
// MARK: - OIDC // MARK: - OIDC

View File

@ -1,65 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Combine
import Foundation
import MatrixRustSDK
class MockAuthenticationService: AuthenticationServiceProtocol {
let validCredentials = (username: "alice", password: "12345678")
private let homeserverSubject: CurrentValueSubject<LoginHomeserver, Never>
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { homeserverSubject.asCurrentValuePublisher() }
init(homeserver: LoginHomeserver = .mockMatrixDotOrg) {
homeserverSubject = .init(homeserver)
}
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
// Map the address to the mock homeservers
if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) {
homeserverSubject.send(.mockMatrixDotOrg)
return .success(())
} else if LoginHomeserver.mockOIDC.address.contains(homeserverAddress) {
homeserverSubject.send(.mockOIDC)
return .success(())
} else if LoginHomeserver.mockBasicServer.address.contains(homeserverAddress) {
homeserverSubject.send(.mockBasicServer)
return .success(())
} else if LoginHomeserver.mockUnsupported.address.contains(homeserverAddress) {
homeserverSubject.send(.mockUnsupported)
return .success(())
} else {
// Otherwise fail with an invalid server.
return .failure(.invalidServer)
}
}
func urlForOIDCLogin() async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError> {
.failure(.oidcError(.notSupported))
}
func abortOIDCLogin(data: OIDCAuthorizationDataProxy) async { }
func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthorizationDataProxy) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
.failure(.oidcError(.notSupported))
}
func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
// Login only succeeds if the username and password match the valid credentials property
guard username == validCredentials.username, password == validCredentials.password else {
return .failure(.invalidCredentials)
}
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: username))))
return .success(userSession)
}
func completeWebRegistration(using credentials: WebRegistrationCredentials) async -> Result<any UserSessionProtocol, AuthenticationServiceError> {
.failure(.failedLoggingIn)
}
}

View File

@ -81,7 +81,7 @@ final class QRCodeLoginService: QRCodeLoginServiceProtocol {
sessionDirectories = .init() sessionDirectories = .init()
} }
private func userSession(for client: Client) async -> Result<UserSessionProtocol, QRCodeLoginServiceError> { private func userSession(for client: ClientProtocol) async -> Result<UserSessionProtocol, QRCodeLoginServiceError> {
switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) { switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) {
case .success(let session): case .success(let session):
return .success(session) return .success(session)

View File

@ -60,7 +60,7 @@ class UserSessionStore: UserSessionStoreProtocol {
} }
} }
func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError> { func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError> {
do { do {
let session = try client.session() let session = try client.session()
let userID = try client.userId() let userID = try client.userId()
@ -146,7 +146,7 @@ class UserSessionStore: UserSessionStoreProtocol {
} }
} }
private func setupProxyForClient(_ client: Client) async -> ClientProxyProtocol { private func setupProxyForClient(_ client: ClientProtocol) async -> ClientProxyProtocol {
await ClientProxy(client: client, await ClientProxy(client: client,
networkMonitor: networkMonitor, networkMonitor: networkMonitor,
appSettings: appSettings) appSettings: appSettings)

View File

@ -14,6 +14,7 @@ enum UserSessionStoreError: Error {
case failedSettingUpSession case failedSettingUpSession
} }
// sourcery: AutoMockable
protocol UserSessionStoreProtocol { protocol UserSessionStoreProtocol {
/// Deletes all data stored in the shared container and keychain /// Deletes all data stored in the shared container and keychain
func reset() func reset()
@ -31,7 +32,7 @@ protocol UserSessionStoreProtocol {
func restoreUserSession() async -> Result<UserSessionProtocol, UserSessionStoreError> func restoreUserSession() async -> Result<UserSessionProtocol, UserSessionStoreError>
/// Creates a user session for a new client from the SDK along with the passphrase used for the data stores. /// Creates a user session for a new client from the SDK along with the passphrase used for the data stores.
func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError> func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError>
/// Logs out of the specified session. /// Logs out of the specified session.
func logout(userSession: UserSessionProtocol) func logout(userSession: UserSessionProtocol)

View File

@ -109,22 +109,16 @@ class MockScreen: Identifiable {
lazy var coordinator: CoordinatorProtocol? = { lazy var coordinator: CoordinatorProtocol? = {
switch id { switch id {
case .login:
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = LoginScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationService(),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .serverSelection: case .serverSelection:
let navigationStackCoordinator = NavigationStackCoordinator() let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = ServerSelectionScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), let coordinator = ServerSelectionScreenCoordinator(parameters: .init(authenticationService: AuthenticationService.mock,
userIndicatorController: ServiceLocator.shared.userIndicatorController, authenticationFlow: .login,
isModallyPresented: true)) slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
userIndicatorController: ServiceLocator.shared.userIndicatorController))
navigationStackCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator return navigationStackCoordinator
case .authenticationFlow: case .authenticationFlow:
let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: MockAuthenticationService(), let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: AuthenticationService.mock,
qrCodeLoginService: QRCodeLoginServiceMock(), qrCodeLoginService: QRCodeLoginServiceMock(),
bugReportService: BugReportServiceMock(), bugReportService: BugReportServiceMock(),
navigationRootCoordinator: navigationRootCoordinator, navigationRootCoordinator: navigationRootCoordinator,

View File

@ -20,7 +20,6 @@ enum UITestsScreenIdentifier: String {
case createPoll case createPoll
case createRoom case createRoom
case createRoomNoUsers case createRoomNoUsers
case login
case roomLayoutBottom case roomLayoutBottom
case roomLayoutMiddle case roomLayoutMiddle
case roomLayoutTop case roomLayoutTop

View File

@ -19,6 +19,10 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase {
// Server Confirmation: Tap continue button // Server Confirmation: Tap continue button
app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap() app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap()
// Login Screen: Wait for continue button to appear
let continueButton = app.buttons[A11yIdentifiers.loginScreen.continue]
XCTAssertTrue(continueButton.waitForExistence(timeout: 2.0))
// Login Screen: Enter valid credentials // Login Screen: Enter valid credentials
app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("alice\n") app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("alice\n")
app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("12345678") app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("12345678")
@ -39,20 +43,43 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase {
// Server Confirmation: Tap continue button // Server Confirmation: Tap continue button
app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap() app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap()
// Login Screen: Wait for continue button to appear
let continueButton = app.buttons[A11yIdentifiers.loginScreen.continue]
XCTAssertTrue(continueButton.waitForExistence(timeout: 2.0))
// Login Screen: Enter invalid credentials // Login Screen: Enter invalid credentials
app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("alice") app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("alice")
app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("87654321") app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("87654321")
// Login Screen: Tap next // Login Screen: Tap continue
let nextButton = app.buttons[A11yIdentifiers.loginScreen.continue] XCTAssertTrue(continueButton.isEnabled)
XCTAssertTrue(nextButton.waitForExistence(timeout: 2.0)) continueButton.tap()
XCTAssertTrue(nextButton.isEnabled)
nextButton.tap()
// Then login should fail. // Then login should fail.
XCTAssertTrue(app.alerts.element.waitForExistence(timeout: 2.0), "An error alert should be shown when attempting login with invalid credentials.") XCTAssertTrue(app.alerts.element.waitForExistence(timeout: 2.0), "An error alert should be shown when attempting login with invalid credentials.")
} }
func testLoginWithUnsupportedUserID() async throws {
// Given the authentication flow.
let app = Application.launch(.authenticationFlow)
// Splash Screen: Tap get started button
app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap()
// Server Confirmation: Tap continue button
app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap()
// Login Screen: Wait for continue button to appear
let continueButton = app.buttons[A11yIdentifiers.loginScreen.continue]
XCTAssertTrue(continueButton.waitForExistence(timeout: 2.0))
// When entering a username on a homeserver with an unsupported flow.
app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("@test:server.net\n")
// Then the screen should not allow login to continue.
try await app.assertScreenshot(.authenticationFlow, step: 1)
}
func testSelectingOIDCServer() { func testSelectingOIDCServer() {
// Given the authentication flow. // Given the authentication flow.
let app = Application.launch(.authenticationFlow) let app = Application.launch(.authenticationFlow)

View File

@ -1,35 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import XCTest
@MainActor
class LoginScreenUITests: XCTestCase {
func testMatrixDotOrg() async throws {
// Given the initial login screen which defaults to matrix.org.
let app = Application.launch(.login)
try await app.assertScreenshot(.login)
// When typing in a username and password.
app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("@test:matrix.org")
app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("12345678")
// Then the form should be ready to submit.
try await app.assertScreenshot(.login, step: 0)
}
func testUnsupported() async throws {
// Given the initial login screen.
let app = Application.launch(.login)
// When entering a username on a homeserver with an unsupported flow.
app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("@test:server.net\n")
// Then the screen should not allow login to continue.
try await app.assertScreenshot(.login, step: 1)
}
}

Binary file not shown.

View File

@ -0,0 +1,96 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import XCTest
@testable import ElementX
class AuthenticationServiceTests: XCTestCase {
var client: ClientSDKMock!
var userSessionStore: UserSessionStoreMock!
var encryptionKeyProvider: MockEncryptionKeyProvider!
var service: AuthenticationService!
func testLogin() async {
setupMocks()
switch await service.configure(for: "matrix.org", flow: .login) {
case .success:
break
case .failure(let error):
XCTFail("Unexpected failure: \(error)")
}
XCTAssertEqual(service.flow, .login)
XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg)
switch await service.login(username: "alice", password: "12345678", initialDeviceName: nil, deviceID: nil) {
case .success:
XCTAssertEqual(client.loginUsernamePasswordInitialDeviceNameDeviceIdCallsCount, 1)
XCTAssertEqual(userSessionStore.userSessionForSessionDirectoriesPassphraseCallsCount, 1)
XCTAssertEqual(userSessionStore.userSessionForSessionDirectoriesPassphraseReceivedArguments?.passphrase,
encryptionKeyProvider.generateKey().base64EncodedString())
case .failure(let error):
XCTFail("Unexpected failure: \(error)")
}
}
func testConfigureRegister() async {
setupMocks()
switch await service.configure(for: "matrix.org", flow: .register) {
case .success:
break
case .failure(let error):
XCTFail("Unexpected failure: \(error)")
}
XCTAssertEqual(service.flow, .register)
XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg)
}
func testConfigureRegisterNoSupport() async {
let homeserverAddress = "example.com"
setupMocks(serverAddress: homeserverAddress)
switch await service.configure(for: homeserverAddress, flow: .register) {
case .success:
XCTFail("Configuration should have failed")
case .failure(let error):
XCTAssertEqual(error, .registrationNotSupported)
}
XCTAssertEqual(service.flow, .login)
XCTAssertEqual(service.homeserver.value, .init(address: "matrix.org", loginMode: .unknown))
}
// MARK: - Helpers
private func setupMocks(serverAddress: String = "matrix.org") {
let configuration: AuthenticationClientBuilderMock.Configuration = .init()
let clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init(builderConfiguration: configuration))
client = configuration.homeserverClients[serverAddress]
userSessionStore = UserSessionStoreMock(configuration: .init())
encryptionKeyProvider = MockEncryptionKeyProvider()
service = AuthenticationService(userSessionStore: userSessionStore,
encryptionKeyProvider: encryptionKeyProvider,
clientBuilderFactory: clientBuilderFactory,
appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks())
}
}
struct MockEncryptionKeyProvider: EncryptionKeyProviderProtocol {
private let key = "12345678"
func generateKey() -> Data {
Data(key.utf8)
}
}

View File

@ -26,18 +26,11 @@ class ServerConfirmationScreenViewStateTests: XCTestCase {
func testRegisterMessageString() { func testRegisterMessageString() {
let matrixDotOrgRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockMatrixDotOrg.address, let matrixDotOrgRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockMatrixDotOrg.address,
authenticationFlow: .register, authenticationFlow: .register)
homeserverSupportsRegistration: true)
XCTAssertEqual(matrixDotOrgRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.") XCTAssertEqual(matrixDotOrgRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.")
let oidcRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockOIDC.address, let oidcRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockOIDC.address,
authenticationFlow: .register, authenticationFlow: .register)
homeserverSupportsRegistration: true)
XCTAssertEqual(oidcRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.") XCTAssertEqual(oidcRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.")
let otherRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockBasicServer.address,
authenticationFlow: .register,
homeserverSupportsRegistration: false)
XCTAssertEqual(otherRegister.message, L10n.errorAccountCreationNotPossible, "The registration message should always be the same.")
} }
} }

View File

@ -11,5 +11,117 @@ import XCTest
@MainActor @MainActor
class ServerConfirmationScreenViewModelTests: XCTestCase { class ServerConfirmationScreenViewModelTests: XCTestCase {
// Nothing to test, the view model has no mutable state. var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
var service: AuthenticationServiceProtocol!
var viewModel: ServerConfirmationScreenViewModel!
var context: ServerConfirmationScreenViewModel.Context { viewModel.context }
func testConfirmLoginWithoutConfiguration() async throws {
// Given a view model for login using a service that hasn't been configured.
setupViewModel(authenticationFlow: .login)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
// When continuing from the confirmation screen.
let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm }
context.send(viewAction: .confirm)
try await deferred.fulfill()
// Then a call to configure service should be made.
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown)
}
func testConfirmLoginAfterConfiguration() async throws {
// Given a view model for login using a service that has already been configured (via the server selection screen).
setupViewModel(authenticationFlow: .login)
guard case .success = await service.configure(for: viewModel.state.homeserverAddress, flow: .login) else {
XCTFail("The configuration should succeed.")
return
}
XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown)
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
// When continuing from the confirmation screen.
let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm }
context.send(viewAction: .confirm)
try await deferred.fulfill()
// Then the configured homeserver should be used and no additional call should be made to the service.
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
}
func testConfirmRegisterWithoutConfiguration() async throws {
// Given a view model for registration using a service that hasn't been configured.
setupViewModel(authenticationFlow: .register)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
// When continuing from the confirmation screen.
let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm }
context.send(viewAction: .confirm)
try await deferred.fulfill()
// Then a call to configure service should be made.
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown)
}
func testConfirmRegisterAfterConfiguration() async throws {
// Given a view model for registration using a service that has already been configured (via the server selection screen).
setupViewModel(authenticationFlow: .register)
guard case .success = await service.configure(for: viewModel.state.homeserverAddress, flow: .register) else {
XCTFail("The configuration should succeed.")
return
}
XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown)
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
// When continuing from the confirmation screen.
let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm }
context.send(viewAction: .confirm)
try await deferred.fulfill()
// Then the configured homeserver should be used and no additional call should be made to the service.
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
}
func testRegistrationNotSupportedAlert() async throws {
// Given a view model for registration using a service that hasn't been configured and the default server doesn't support registration.
setupViewModel(authenticationFlow: .register, elementWellKnown: false)
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
XCTAssertFalse(service.homeserver.value.supportsRegistration)
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
XCTAssertNil(context.alertInfo)
// When continuing from the confirmation screen.
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
context.send(viewAction: .confirm)
try await deferred.fulfill()
// Then the configured homeserver should be used and no additional call should be made to the service.
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
XCTAssertEqual(context.alertInfo?.id, .registration)
}
// MARK: - Helpers
private func setupViewModel(authenticationFlow: AuthenticationFlow, elementWellKnown: Bool = true) {
let client = ClientSDKMock(configuration: elementWellKnown ? .init() : .init(elementWellKnown: ""))
let configuration = AuthenticationClientBuilderMock.Configuration(homeserverClients: ["matrix.org": client],
qrCodeClient: client)
clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init(builderConfiguration: configuration))
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
encryptionKeyProvider: EncryptionKeyProvider(),
clientBuilderFactory: clientBuilderFactory,
appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks())
viewModel = ServerConfirmationScreenViewModel(authenticationService: service,
authenticationFlow: authenticationFlow,
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
userIndicatorController: UserIndicatorControllerMock())
}
} }

View File

@ -16,8 +16,7 @@ class ServerSelectionViewModelTests: XCTestCase {
@MainActor override func setUp() { @MainActor override func setUp() {
viewModel = ServerSelectionScreenViewModel(homeserverAddress: "", viewModel = ServerSelectionScreenViewModel(homeserverAddress: "",
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
isModallyPresented: true)
context = viewModel.context context = viewModel.context
} }