Move the core logic in LoginScreenCoordinator into the ViewModel. (#3348)

This commit is contained in:
Doug 2024-10-01 13:09:45 +01:00 committed by GitHub
parent 5f4c2890f6
commit 268d9f7479
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 382 additions and 388 deletions

View File

@ -144,7 +144,6 @@
1D623953F970D11F6F38499C /* AppLockService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851B95BB98649B8E773D6790 /* AppLockService.swift */; }; 1D623953F970D11F6F38499C /* AppLockService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851B95BB98649B8E773D6790 /* AppLockService.swift */; };
1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */; }; 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */; };
1DC227816777A2F3A19657E5 /* RoomDirectorySearchScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCF71646898A2F720C5BFDF5 /* RoomDirectorySearchScreenViewModel.swift */; }; 1DC227816777A2F3A19657E5 /* RoomDirectorySearchScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCF71646898A2F720C5BFDF5 /* RoomDirectorySearchScreenViewModel.swift */; };
1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; };
1F3232BD368DF430AB433907 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = 07FEEEDB11543A7DED420F04 /* Compound */; }; 1F3232BD368DF430AB433907 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = 07FEEEDB11543A7DED420F04 /* Compound */; };
1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; }; 1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; };
1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */; }; 1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */; };
@ -463,6 +462,7 @@
67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; 67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; };
67D6E0700A9C1E676F6231F8 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = AD544C0FA48DFFB080920061 /* Collections */; }; 67D6E0700A9C1E676F6231F8 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = AD544C0FA48DFFB080920061 /* Collections */; };
67E9926C4572C54F59FCA91A /* AuthenticationFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B069D7772DDF6513E0F1B8 /* AuthenticationFlowCoordinator.swift */; }; 67E9926C4572C54F59FCA91A /* AuthenticationFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B069D7772DDF6513E0F1B8 /* AuthenticationFlowCoordinator.swift */; };
67ECD32538F6DAFE38A623F9 /* ServerSelectionScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45EBAFF1A83538D54ABDF92 /* ServerSelectionScreenViewModelTests.swift */; };
67EFF46180B939CBF389AECD /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C713D124FE915ABF47A6B7 /* TimelineView.swift */; }; 67EFF46180B939CBF389AECD /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C713D124FE915ABF47A6B7 /* TimelineView.swift */; };
6817EAD73DC1FFD8B943B5B9 /* HomeScreenRoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73587C2E3CF5998361AE516 /* HomeScreenRoomTests.swift */; }; 6817EAD73DC1FFD8B943B5B9 /* HomeScreenRoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73587C2E3CF5998361AE516 /* HomeScreenRoomTests.swift */; };
68184EF36396424FE19A727D /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; }; 68184EF36396424FE19A727D /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; };
@ -513,6 +513,7 @@
73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */; }; 73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */; };
73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */; }; 73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */; };
7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; }; 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; };
7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */; };
743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */; }; 743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */; };
748F482FEF4E04D61C39AAD7 /* EmojiPickerScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */; }; 748F482FEF4E04D61C39AAD7 /* EmojiPickerScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */; };
7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */; }; 7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */; };
@ -668,7 +669,6 @@
92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */; }; 92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */; };
934051B17A884AB0635DF81B /* BlockedUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A010B8EAD1A9F6B4686DF2F4 /* BlockedUsersScreenViewModel.swift */; }; 934051B17A884AB0635DF81B /* BlockedUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A010B8EAD1A9F6B4686DF2F4 /* BlockedUsersScreenViewModel.swift */; };
937985546F708339711ECDFC /* ComposerToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85666E40F7E817809B4FD787 /* ComposerToolbar.swift */; }; 937985546F708339711ECDFC /* ComposerToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85666E40F7E817809B4FD787 /* ComposerToolbar.swift */; };
93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */; };
93A549135E6C027A0D823BFE /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 593FBBF394712F2963E98A0B /* DTCoreText */; }; 93A549135E6C027A0D823BFE /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 593FBBF394712F2963E98A0B /* DTCoreText */; };
93AC1E8418D8C827671FB3A9 /* IdentityConfirmedScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595EC503DA5517BBE6D39406 /* IdentityConfirmedScreenCoordinator.swift */; }; 93AC1E8418D8C827671FB3A9 /* IdentityConfirmedScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595EC503DA5517BBE6D39406 /* IdentityConfirmedScreenCoordinator.swift */; };
93BA4A81B6D893271101F9F0 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = A7CA6F33C553805035C3B114 /* DeviceKit */; }; 93BA4A81B6D893271101F9F0 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = A7CA6F33C553805035C3B114 /* DeviceKit */; };
@ -1607,6 +1607,7 @@
5A1119E9C63AE530252640D2 /* SecureBackupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupController.swift; sourceTree = "<group>"; }; 5A1119E9C63AE530252640D2 /* SecureBackupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupController.swift; sourceTree = "<group>"; };
5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreen.swift; sourceTree = "<group>"; }; 5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreen.swift; sourceTree = "<group>"; };
5A37E2FACFD041CE466223CD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; }; 5A37E2FACFD041CE466223CD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = "<group>"; };
5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = "<group>"; }; 5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = "<group>"; };
5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = "<group>"; }; 5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = "<group>"; };
5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenCoordinator.swift; sourceTree = "<group>"; }; 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenCoordinator.swift; sourceTree = "<group>"; };
@ -1888,7 +1889,6 @@
A010B8EAD1A9F6B4686DF2F4 /* BlockedUsersScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModel.swift; sourceTree = "<group>"; }; A010B8EAD1A9F6B4686DF2F4 /* BlockedUsersScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModel.swift; sourceTree = "<group>"; };
A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModel.swift; sourceTree = "<group>"; }; A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModel.swift; sourceTree = "<group>"; };
A02D1A490944BF01A37586E1 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/SAS.strings; sourceTree = "<group>"; }; A02D1A490944BF01A37586E1 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/SAS.strings; sourceTree = "<group>"; };
A05707BF550D770168A406DB /* LoginViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = "<group>"; };
A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = "<group>"; }; A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = "<group>"; };
A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = "<group>"; }; A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = "<group>"; };
A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = "<group>"; }; A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@ -2186,6 +2186,7 @@
E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModel.swift; sourceTree = "<group>"; }; E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModel.swift; sourceTree = "<group>"; };
E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorModalView.swift; sourceTree = "<group>"; }; E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorModalView.swift; sourceTree = "<group>"; };
E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverter.swift; sourceTree = "<group>"; }; E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverter.swift; sourceTree = "<group>"; };
E45EBAFF1A83538D54ABDF92 /* ServerSelectionScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModelTests.swift; sourceTree = "<group>"; };
E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteURLParserTests.swift; sourceTree = "<group>"; }; E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteURLParserTests.swift; sourceTree = "<group>"; };
E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; }; E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = "<group>"; }; E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = "<group>"; };
@ -2233,7 +2234,6 @@
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; }; ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; };
ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = "<group>"; }; ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = "<group>"; };
ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = "<group>"; }; ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = "<group>"; };
EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = "<group>"; };
EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; };
EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenScreenViewModelTests.swift; sourceTree = "<group>"; }; EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenScreenViewModelTests.swift; sourceTree = "<group>"; };
EEAA2832D93EC7D2608703FB /* NSEUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEUserSession.swift; sourceTree = "<group>"; }; EEAA2832D93EC7D2608703FB /* NSEUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEUserSession.swift; sourceTree = "<group>"; };
@ -3828,7 +3828,7 @@
6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */, 6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */,
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */, C070FD43DC6BF4E50217965A /* LocalizationTests.swift */,
3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */, 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */,
A05707BF550D770168A406DB /* LoginViewModelTests.swift */, 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */,
376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */, 376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */,
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */, F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */,
2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */, 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */,
@ -3870,7 +3870,7 @@
40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */, 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */,
277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */, 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */,
F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */, F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */,
EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */, E45EBAFF1A83538D54ABDF92 /* ServerSelectionScreenViewModelTests.swift */,
0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */, 0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */,
A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */, A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */,
DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */, DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */,
@ -6122,7 +6122,7 @@
8AC256AF0EC54658321C9241 /* LegalInformationScreenViewModelTests.swift in Sources */, 8AC256AF0EC54658321C9241 /* LegalInformationScreenViewModelTests.swift in Sources */,
0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */,
149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */, 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */,
1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */, 7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */,
77C1A2F49CD90D3EFDF376E5 /* MapTilerURLBuildersTests.swift in Sources */, 77C1A2F49CD90D3EFDF376E5 /* MapTilerURLBuildersTests.swift in Sources */,
2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */, 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */,
4B978C09567387EF4366BD7A /* MediaLoaderTests.swift in Sources */, 4B978C09567387EF4366BD7A /* MediaLoaderTests.swift in Sources */,
@ -6171,7 +6171,7 @@
1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */, 1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */,
53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */, 53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */,
89658A44C9FC19B58FD1C226 /* ServerConfirmationScreenViewModelTests.swift in Sources */, 89658A44C9FC19B58FD1C226 /* ServerConfirmationScreenViewModelTests.swift in Sources */,
93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */, 67ECD32538F6DAFE38A623F9 /* ServerSelectionScreenViewModelTests.swift in Sources */,
CC1C948F67A5510A340FD7F0 /* SessionDirectoriesTests.swift in Sources */, CC1C948F67A5510A340FD7F0 /* SessionDirectoriesTests.swift in Sources */,
86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */, 86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */,
755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */, 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */,

View File

@ -244,8 +244,9 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
private func showLoginScreen() { private func showLoginScreen() {
let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService, let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService,
analytics: analytics, slidingSyncLearnMoreURL: appSettings.slidingSyncLearnMoreURL,
userIndicatorController: userIndicatorController) userIndicatorController: userIndicatorController,
analytics: analytics)
let coordinator = LoginScreenCoordinator(parameters: parameters) let coordinator = LoginScreenCoordinator(parameters: parameters)
coordinator.actions coordinator.actions

View File

@ -20,10 +20,8 @@ enum LoginMode: Equatable {
var supportsOIDCFlow: Bool { var supportsOIDCFlow: Bool {
switch self { switch self {
case .oidc: case .oidc: true
return true default: false
default:
return false
} }
} }
} }

View File

@ -11,9 +11,9 @@ import SwiftUI
struct LoginScreenCoordinatorParameters { struct LoginScreenCoordinatorParameters {
/// The service used to authenticate the user. /// The service used to authenticate the user.
let authenticationService: AuthenticationServiceProtocol let authenticationService: AuthenticationServiceProtocol
let slidingSyncLearnMoreURL: URL
let analytics: AnalyticsService
let userIndicatorController: UserIndicatorControllerProtocol let userIndicatorController: UserIndicatorControllerProtocol
let analytics: AnalyticsService
} }
enum LoginScreenCoordinatorAction { enum LoginScreenCoordinatorAction {
@ -42,8 +42,10 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
init(parameters: LoginScreenCoordinatorParameters) { init(parameters: LoginScreenCoordinatorParameters) {
self.parameters = parameters self.parameters = parameters
viewModel = LoginScreenViewModel(homeserver: parameters.authenticationService.homeserver.value, viewModel = LoginScreenViewModel(authenticationService: parameters.authenticationService,
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL) slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL,
userIndicatorController: parameters.userIndicatorController,
analytics: parameters.analytics)
} }
// MARK: - Public // MARK: - Public
@ -54,119 +56,20 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .parseUsername(let username): case .configuredForOIDC:
parseUsername(username) actionsSubject.send(.configuredForOIDC)
case .forgotPassword: case .signedIn(let userSession):
showForgotPasswordScreen() actionsSubject.send(.signedIn(userSession))
case .login(let username, let password):
login(username: username, password: password)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
func stop() { func stop() {
stopLoading() viewModel.stopLoading()
} }
func toPresentable() -> AnyView { func toPresentable() -> AnyView {
AnyView(LoginScreen(context: viewModel.context)) AnyView(LoginScreen(context: viewModel.context))
} }
// MARK: - Private
private static let loadingIndicatorIdentifier = "\(LoginScreenCoordinatorAction.self)-Loading"
private func startLoading(isInteractionBlocking: Bool) {
if isInteractionBlocking {
parameters.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
type: .modal,
title: L10n.commonLoading,
persistent: true))
} else {
viewModel.update(isLoading: true)
}
}
private func stopLoading() {
viewModel.update(isLoading: false)
parameters.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
}
/// Processes an error to either update the flow or display it to the user.
private func handleError(_ error: AuthenticationServiceError) {
MXLog.info("Error occurred: \(error)")
switch error {
case .invalidCredentials:
viewModel.displayError(.alert(L10n.screenLoginErrorInvalidCredentials))
case .accountDeactivated:
viewModel.displayError(.alert(L10n.screenLoginErrorDeactivatedAccount))
case .invalidWellKnown(let error):
viewModel.displayError(.invalidWellKnownAlert(error))
case .slidingSyncNotAvailable:
viewModel.displayError(.slidingSyncAlert)
case .sessionTokenRefreshNotSupported:
viewModel.displayError(.refreshTokenAlert)
default:
viewModel.displayError(.alert(L10n.errorUnknown))
}
}
/// Requests the authentication coordinator to log in using the specified credentials.
private func login(username: String, password: String) {
MXLog.info("Starting login with password.")
startLoading(isInteractionBlocking: true)
Task {
parameters.analytics.signpost.beginLogin()
switch await authenticationService.login(username: username,
password: password,
initialDeviceName: UIDevice.current.initialDeviceName,
deviceID: nil) {
case .success(let userSession):
actionsSubject.send(.signedIn(userSession))
parameters.analytics.signpost.endLogin()
stopLoading()
case .failure(let error):
stopLoading()
parameters.analytics.signpost.endLogin()
handleError(error)
}
}
}
/// Parses the specified username and looks up the homeserver when a Matrix ID is entered.
private func parseUsername(_ username: String) {
guard MatrixEntityRegex.isMatrixUserIdentifier(username) else { return }
let homeserverDomain = String(username.split(separator: ":")[1])
startLoading(isInteractionBlocking: false)
Task {
switch await authenticationService.configure(for: homeserverDomain, flow: .login) {
case .success:
stopLoading()
if authenticationService.homeserver.value.loginMode == .oidc {
actionsSubject.send(.configuredForOIDC)
} else {
updateViewModel()
}
case .failure(let error):
stopLoading()
handleError(error)
}
}
}
/// Updates the view model with a different homeserver.
private func updateViewModel() {
viewModel.update(homeserver: authenticationService.homeserver.value)
}
/// Shows the forgot password screen.
private func showForgotPasswordScreen() {
viewModel.displayError(.alert("Not implemented."))
}
} }

View File

@ -7,23 +7,16 @@
import Foundation import Foundation
enum LoginScreenViewModelAction: CustomStringConvertible { enum LoginScreenViewModelAction {
/// Parse the username and update the homeserver if included. /// The homeserver was updated to one that supports OIDC.
case parseUsername(String) case configuredForOIDC
/// The user would like to reset their password. /// Login was successful.
case forgotPassword case signedIn(UserSessionProtocol)
/// Login using the supplied credentials.
case login(username: String, password: String)
/// A string representation of the action, ignoring any associated values that could leak PII. var isConfiguredForOIDC: Bool {
var description: String {
switch self { switch self {
case .parseUsername: case .configuredForOIDC: true
return "parseUsername" default: false
case .forgotPassword:
return "forgotPassword"
case .login:
return "login"
} }
} }
} }
@ -34,7 +27,7 @@ struct LoginScreenViewState: BindableState {
/// Whether a new homeserver is currently being loaded. /// Whether a new homeserver is currently being loaded.
var isLoading = false var isLoading = false
/// View state that can be bound to from SwiftUI. /// View state that can be bound to from SwiftUI.
var bindings: LoginScreenBindings var bindings = LoginScreenBindings()
/// The types of login supported by the homeserver. /// The types of login supported by the homeserver.
var loginMode: LoginMode { homeserver.loginMode } var loginMode: LoginMode { homeserver.loginMode }
@ -62,8 +55,6 @@ struct LoginScreenBindings {
enum LoginScreenViewAction { enum LoginScreenViewAction {
/// Parse the username to detect if a homeserver is included. /// Parse the username to detect if a homeserver is included.
case parseUsername case parseUsername
/// The user would like to reset their password.
case forgotPassword
/// Continue using the input username and password. /// Continue using the input username and password.
case next case next
} }
@ -71,8 +62,10 @@ enum LoginScreenViewAction {
enum LoginScreenErrorType: Hashable { enum LoginScreenErrorType: Hashable {
/// A specific error message shown in an alert. /// A specific error message shown in an alert.
case alert(String) case alert(String)
/// Looking up the homeserver from the username failed. /// An alert that informs the user to check their username/password.
case invalidHomeserver case credentialsAlert
/// An alert that informs the user that their account has been deactivated.
case deactivatedAlert
/// An alert that informs the user about a bad well-known file. /// An alert that informs the user about a bad well-known file.
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.

View File

@ -11,57 +11,129 @@ import SwiftUI
typealias LoginScreenViewModelType = StateStoreViewModel<LoginScreenViewState, LoginScreenViewAction> typealias LoginScreenViewModelType = StateStoreViewModel<LoginScreenViewState, LoginScreenViewAction>
class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtocol { class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtocol {
private let authenticationService: AuthenticationServiceProtocol
private let slidingSyncLearnMoreURL: URL private let slidingSyncLearnMoreURL: URL
private let userIndicatorController: UserIndicatorControllerProtocol
private let analytics: AnalyticsService
private var actionsSubject: PassthroughSubject<LoginScreenViewModelAction, Never> = .init() private var actionsSubject: PassthroughSubject<LoginScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<LoginScreenViewModelAction, Never> { var actions: AnyPublisher<LoginScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
init(homeserver: LoginHomeserver, slidingSyncLearnMoreURL: URL) { init(authenticationService: AuthenticationServiceProtocol,
slidingSyncLearnMoreURL: URL,
userIndicatorController: UserIndicatorControllerProtocol,
analytics: AnalyticsService) {
self.authenticationService = authenticationService
self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL
let bindings = LoginScreenBindings() self.userIndicatorController = userIndicatorController
let viewState = LoginScreenViewState(homeserver: homeserver, bindings: bindings) self.analytics = analytics
let viewState = LoginScreenViewState(homeserver: authenticationService.homeserver.value)
super.init(initialViewState: viewState) super.init(initialViewState: viewState)
authenticationService.homeserver
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state.homeserver, on: self)
.store(in: &cancellables)
} }
override func process(viewAction: LoginScreenViewAction) { override func process(viewAction: LoginScreenViewAction) {
switch viewAction { switch viewAction {
case .parseUsername: case .parseUsername:
actionsSubject.send(.parseUsername(state.bindings.username)) parseUsername()
case .forgotPassword:
actionsSubject.send(.forgotPassword)
case .next: case .next:
actionsSubject.send(.login(username: state.bindings.username, password: state.bindings.password)) login()
} }
} }
func update(isLoading: Bool) { func stopLoading() {
guard state.isLoading != isLoading else { return } state.isLoading = false
state.isLoading = isLoading userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
} }
func update(homeserver: LoginHomeserver) { // MARK: - Private
state.homeserver = homeserver
/// Parses the specified username and looks up the homeserver when a Matrix ID is entered.
private func parseUsername() {
let username = state.bindings.username
guard MatrixEntityRegex.isMatrixUserIdentifier(username) else { return }
let homeserverDomain = String(username.split(separator: ":")[1])
startLoading(isInteractionBlocking: false)
Task {
switch await authenticationService.configure(for: homeserverDomain, flow: .login) {
case .success:
if authenticationService.homeserver.value.loginMode == .oidc {
actionsSubject.send(.configuredForOIDC)
}
stopLoading()
case .failure(let error):
stopLoading()
handleError(error)
}
}
} }
func displayError(_ type: LoginScreenErrorType) { /// Requests the authentication coordinator to log in using the specified credentials.
switch type { private func login() {
case .alert(let message): MXLog.info("Starting login with password.")
state.bindings.alertInfo = AlertInfo(id: type, startLoading(isInteractionBlocking: true)
Task {
analytics.signpost.beginLogin()
switch await authenticationService.login(username: state.bindings.username,
password: state.bindings.password,
initialDeviceName: UIDevice.current.initialDeviceName,
deviceID: nil) {
case .success(let userSession):
actionsSubject.send(.signedIn(userSession))
analytics.signpost.endLogin()
stopLoading()
case .failure(let error):
stopLoading()
analytics.signpost.endLogin()
handleError(error)
}
}
}
private static let loadingIndicatorIdentifier = "\(LoginScreenCoordinatorAction.self)-Loading"
private func startLoading(isInteractionBlocking: Bool) {
if isInteractionBlocking {
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
type: .modal,
title: L10n.commonLoading,
persistent: true))
} else {
state.isLoading = true
}
}
/// Processes an error to either update the flow or display it to the user.
private func handleError(_ error: AuthenticationServiceError) {
MXLog.info("Error occurred: \(error)")
switch error {
case .invalidCredentials:
state.bindings.alertInfo = AlertInfo(id: .credentialsAlert,
title: L10n.commonError, title: L10n.commonError,
message: message) message: L10n.screenLoginErrorInvalidCredentials)
case .invalidHomeserver: case .accountDeactivated:
state.bindings.alertInfo = AlertInfo(id: type, state.bindings.alertInfo = AlertInfo(id: .deactivatedAlert,
title: L10n.commonError, title: L10n.commonError,
message: L10n.screenLoginErrorInvalidUserId) message: L10n.screenLoginErrorDeactivatedAccount)
case .invalidWellKnownAlert(let error): case .invalidWellKnown(let error):
state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert, state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert,
title: L10n.commonServerNotSupported, title: L10n.commonServerNotSupported,
message: L10n.screenChangeServerErrorInvalidWellKnown(error)) message: L10n.screenChangeServerErrorInvalidWellKnown(error))
case .slidingSyncAlert: case .slidingSyncNotAvailable:
let openURL = { UIApplication.shared.open(self.slidingSyncLearnMoreURL) } let openURL = { UIApplication.shared.open(self.slidingSyncLearnMoreURL) }
state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert, state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert,
title: L10n.commonServerNotSupported, title: L10n.commonServerNotSupported,
@ -71,12 +143,12 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
// Clear out the invalid username to avoid an attempted login to matrix.org // Clear out the invalid username to avoid an attempted login to matrix.org
state.bindings.username = "" state.bindings.username = ""
case .refreshTokenAlert: case .sessionTokenRefreshNotSupported:
state.bindings.alertInfo = AlertInfo(id: type, state.bindings.alertInfo = AlertInfo(id: .refreshTokenAlert,
title: L10n.commonServerNotSupported, title: L10n.commonServerNotSupported,
message: L10n.screenLoginErrorRefreshTokens) message: L10n.screenLoginErrorRefreshTokens)
case .unknown: default:
state.bindings.alertInfo = AlertInfo(id: type) state.bindings.alertInfo = AlertInfo(id: .unknown)
} }
} }
} }

View File

@ -12,15 +12,6 @@ protocol LoginScreenViewModelProtocol {
var actions: AnyPublisher<LoginScreenViewModelAction, Never> { get } var actions: AnyPublisher<LoginScreenViewModelAction, Never> { get }
var context: LoginScreenViewModelType.Context { get } var context: LoginScreenViewModelType.Context { get }
/// Update the view to reflect that a new homeserver is being loaded. /// Update the view to reflect that loaded has finished.
/// - Parameter isLoading: Whether or not the homeserver is being loaded. func stopLoading()
func update(isLoading: Bool)
/// Update the view with new homeserver information.
/// - Parameter homeserver: The view data for the homeserver. This can be generated using `AuthenticationService.Homeserver.viewData`.
func update(homeserver: LoginHomeserver)
/// Display an error to the user.
/// - Parameter type: The type of error to be displayed.
func displayError(_ type: LoginScreenErrorType)
} }

View File

@ -29,6 +29,7 @@ struct LoginScreen: View {
// This should never be shown. // This should never be shown.
ProgressView() ProgressView()
default: default:
// This should never be shown either.
loginUnavailableText loginUnavailableText
} }
} }
@ -37,6 +38,7 @@ struct LoginScreen: View {
.padding(.bottom, 16) .padding(.bottom, 16)
} }
.background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.navigationBarTitleDisplayMode(.inline)
.alert(item: $context.alertInfo) .alert(item: $context.alertInfo)
} }
@ -124,35 +126,45 @@ struct LoginScreen: View {
// MARK: - Previews // MARK: - Previews
struct LoginScreen_Previews: PreviewProvider, TestablePreview { struct LoginScreen_Previews: PreviewProvider, TestablePreview {
static let credentialsViewModel: LoginScreenViewModel = { static let viewModel = makeViewModel()
let viewModel = LoginScreenViewModel(homeserver: .mockMatrixDotOrg, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL) static let credentialsViewModel = makeViewModel(withCredentials: true)
viewModel.context.username = "alice" static let unconfiguredViewModel = makeViewModel(homeserverAddress: "somethingtofailconfiguration")
viewModel.context.password = "password"
return viewModel
}()
static var previews: some View { static var previews: some View {
screen(for: LoginScreenViewModel(homeserver: .mockMatrixDotOrg, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL))
.previewDisplayName("matrix.org")
screen(for: credentialsViewModel)
.previewDisplayName("Credentials Entered")
screen(for: LoginScreenViewModel(homeserver: .mockMatrixDotOrg, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL))
.previewDisplayName("Unsupported")
screen(for: LoginScreenViewModel(homeserver: .mockMatrixDotOrg, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL))
.previewDisplayName("OIDC Fallback")
}
static func screen(for viewModel: LoginScreenViewModel) -> some View {
NavigationStack { NavigationStack {
LoginScreen(context: viewModel.context) LoginScreen(context: viewModel.context)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button { } label: {
Text("\(Image(systemName: "chevron.backward")) Back")
}
}
}
} }
.previewDisplayName("matrix.org")
.snapshotPreferences(delay: 0.1)
NavigationStack {
LoginScreen(context: credentialsViewModel.context)
}
.previewDisplayName("Credentials Entered")
.snapshotPreferences(delay: 0.1)
NavigationStack {
LoginScreen(context: unconfiguredViewModel.context)
}
.previewDisplayName("Unsupported")
.snapshotPreferences(delay: 0.1)
}
static func makeViewModel(homeserverAddress: String = "matrix.org", withCredentials: Bool = false) -> LoginScreenViewModel {
let authenticationService = AuthenticationService.mock
Task { await authenticationService.configure(for: homeserverAddress, flow: .login) }
let viewModel = LoginScreenViewModel(authenticationService: authenticationService,
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
userIndicatorController: UserIndicatorControllerMock(),
analytics: ServiceLocator.shared.analytics)
if withCredentials {
viewModel.context.username = "alice"
viewModel.context.password = "password"
}
return viewModel
} }
} }

View File

@ -37,10 +37,8 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
authenticationService.homeserver authenticationService.homeserver
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] homeserver in .map(\.address)
guard let self else { return } .weakAssign(to: \.state.homeserverAddress, on: self)
state.homeserverAddress = homeserver.address
}
.store(in: &cancellables) .store(in: &cancellables)
} }

View File

@ -204,9 +204,11 @@ class AuthenticationService: AuthenticationServiceProtocol {
// MARK: - Mocks // MARK: - Mocks
extension AuthenticationService { extension AuthenticationService {
static var mock = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()), static var mock: AuthenticationService {
encryptionKeyProvider: EncryptionKeyProvider(), AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
clientBuilderFactory: AuthenticationClientBuilderFactoryMock(configuration: .init()), encryptionKeyProvider: EncryptionKeyProvider(),
appSettings: ServiceLocator.shared.settings, clientBuilderFactory: AuthenticationClientBuilderFactoryMock(configuration: .init()),
appHooks: AppHooks()) appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks())
}
} }

View File

@ -0,0 +1,173 @@
//
// 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
@testable import ElementX
@MainActor
class LoginScreenViewModelTests: XCTestCase {
var viewModel: LoginScreenViewModelProtocol!
var context: LoginScreenViewModelType.Context { viewModel.context }
var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
var service: AuthenticationServiceProtocol!
private func setupViewModel(homeserverAddress: String = "matrix.org") async {
clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init())
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
encryptionKeyProvider: EncryptionKeyProvider(),
clientBuilderFactory: clientBuilderFactory,
appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks())
guard case .success = await service.configure(for: homeserverAddress, flow: .login) else {
XCTFail("A valid server should be configured for the test.")
return
}
viewModel = LoginScreenViewModel(authenticationService: service,
slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL,
userIndicatorController: UserIndicatorControllerMock(),
analytics: ServiceLocator.shared.analytics)
}
func testMatrixDotOrg() async {
// Given the initial view model configured for matrix.org.
await setupViewModel()
// Then the view state should contain a homeserver that matches matrix.org and show the login form.
XCTAssertEqual(context.viewState.homeserver, .mockMatrixDotOrg, "The homeserver data should match the default homeserver.")
XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.")
}
func testBasicServer() async {
// Given the view model configured for a basic server example.com that only supports password authentication.
await setupViewModel(homeserverAddress: "example.com")
// Then the view state should be updated with the homeserver and show the login form.
XCTAssertEqual(context.viewState.homeserver, .mockBasicServer, "The homeserver data should should match the new homeserver.")
XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.")
}
func testUsernameWithEmptyPassword() async {
// Given a form with an empty username and password.
await setupViewModel()
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
// When entering a username without a password.
context.username = "bob"
context.password = ""
// Then the credentials should be considered invalid.
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
}
func testEmptyUsernameWithPassword() async {
// Given a form with an empty username and password.
await setupViewModel()
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
// When entering a password without a username.
context.username = ""
context.password = "12345678"
// Then the credentials should be considered invalid.
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
}
func testValidCredentials() async {
// Given a form with an empty username and password.
await setupViewModel()
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
// When entering a username and an 8-character password.
context.username = "bob"
context.password = "12345678"
// Then the credentials should be considered valid.
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid when the username and password are valid.")
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
}
func testLoadingServerWithoutPassword() async throws {
// Given a form with valid credentials.
await setupViewModel()
context.username = "@bob:example.com"
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be not be valid without a password.")
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
XCTAssertFalse(context.viewState.canSubmit, "The form should not be submittable.")
// When updating the view model whilst loading a homeserver.
let deferred = deferFulfillment(context.$viewState, keyPath: \.isLoading, transitionValues: [true, false])
context.send(viewAction: .parseUsername)
// Then the view state should represent the loading but never allow submitting to occur.
try await deferred.fulfill()
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
XCTAssertFalse(context.viewState.canSubmit, "The form should still not be submittable.")
}
func testLoadingServerWithPasswordEntered() async throws {
// Given a form with valid credentials.
await setupViewModel()
context.username = "@bob:example.com"
context.password = "12345678"
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.")
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
// When updating the view model whilst loading a homeserver.
let deferred = deferFulfillment(context.$viewState, keyPath: \.canSubmit, transitionValues: [false, true])
context.send(viewAction: .parseUsername)
// Then the view should be blocked from submitting while loading and then become unblocked again.
try await deferred.fulfill()
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
}
func testOIDCServer() async throws {
// Given the screen configured for matrix.org
await setupViewModel()
// When entering a username for a user on a homeserver with OIDC.
let deferred = deferFulfillment(viewModel.actions) { $0.isConfiguredForOIDC }
context.username = "@bob:company.com"
context.send(viewAction: .parseUsername)
try await deferred.fulfill()
// Then the view state should be updated with the homeserver and show the OIDC button.
XCTAssertTrue(context.viewState.loginMode.supportsOIDCFlow, "The OIDC button should be shown.")
}
func testUnsupportedServer() async throws {
// Given the screen configured for matrix.org
await setupViewModel()
XCTAssertNil(context.alertInfo, "There shouldn't be an alert when the screen loads.")
// When entering a username for an unsupported homeserver.
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
context.username = "@bob:server.net"
context.send(viewAction: .parseUsername)
try await deferred.fulfill()
// Then the view state should be updated to show an alert.
XCTAssertEqual(context.alertInfo?.id, .unknown, "An alert should be shown to the user.")
}
}

View File

@ -1,137 +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
@testable import ElementX
@MainActor
class LoginViewModelTests: XCTestCase {
let defaultHomeserver = LoginHomeserver.mockMatrixDotOrg
var viewModel: LoginScreenViewModelProtocol!
var context: LoginScreenViewModelType.Context!
@MainActor override func setUp() async throws {
viewModel = LoginScreenViewModel(homeserver: defaultHomeserver, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)
context = viewModel.context
}
func testMatrixDotOrg() {
// Given the initial view model configured for matrix.org.
let homeserver = defaultHomeserver
// Then the view state should contain a homeserver that matches matrix.org and show the login form.
XCTAssertEqual(context.viewState.homeserver, homeserver, "The homeserver data should match the original.")
XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.")
}
func testBasicServer() {
// Given a basic server example.com that only supports password registration.
let homeserver = LoginHomeserver.mockBasicServer
// When updating the view model with the server.
viewModel.update(homeserver: homeserver)
// Then the view state should be updated with the homeserver and show the login form.
XCTAssertEqual(context.viewState.homeserver, homeserver, "The homeserver data should should match the new homeserver.")
XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.")
}
func testUsernameWithEmptyPassword() {
// Given a form with an empty username and password.
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
// When entering a username without a password.
context.username = "bob"
context.password = ""
// Then the credentials should be considered invalid.
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
}
func testEmptyUsernameWithPassword() {
// Given a form with an empty username and password.
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
// When entering a password without a username.
context.username = ""
context.password = "12345678"
// Then the credentials should be considered invalid.
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
}
func testValidCredentials() {
// Given a form with an empty username and password.
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
// When entering a username and an 8-character password.
context.username = "bob"
context.password = "12345678"
// Then the credentials should be considered valid.
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid when the username and password are valid.")
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
}
func testLoadingServer() {
// Given a form with valid credentials.
context.username = "bob"
context.password = "12345678"
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.")
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
// When updating the view model whilst loading a homeserver.
viewModel.update(isLoading: true)
// Then the view state should reflect that the homeserver is loading.
XCTAssertTrue(context.viewState.isLoading, "The view should now be in a loading state.")
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
// When updating the view model after loading a homeserver.
viewModel.update(isLoading: false)
// Then the view state should reflect that the homeserver is now loaded.
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
}
func testOIDCServer() {
// Given a basic server example.com that supports OIDC registration.
let homeserver = LoginHomeserver.mockOIDC
// When updating the view model with the server.
viewModel.update(homeserver: homeserver)
// Then the view state should be updated with the homeserver and show the OIDC button.
XCTAssertTrue(context.viewState.loginMode.supportsOIDCFlow, "The OIDC button should be shown.")
}
func testLogsForPassword() {
// Given the coordinator and view model results that contain passwords.
let password = "supersecretpassword"
let viewModelAction: LoginScreenViewModelAction = .login(username: "Alice", password: password)
// When creating a string representation of those results (e.g. for logging).
let viewModelActionString = "\(viewModelAction)"
// Then the password should not be included in that string.
XCTAssertFalse("\(viewModelActionString)".contains(password), "The password must not be included in any strings.")
}
}

View File

@ -10,7 +10,7 @@ import XCTest
@testable import ElementX @testable import ElementX
@MainActor @MainActor
class ServerSelectionViewModelTests: XCTestCase { class ServerSelectionScreenViewModelTests: XCTestCase {
var clientBuilderFactory: AuthenticationClientBuilderFactoryMock! var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
var service: AuthenticationServiceProtocol! var service: AuthenticationServiceProtocol!