mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Move the core logic in LoginScreenCoordinator into the ViewModel. (#3348)
This commit is contained in:
parent
5f4c2890f6
commit
268d9f7479
@ -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 */,
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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."))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.Credentials-Entered.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.Credentials-Entered.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.OIDC-Fallback.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.OIDC-Fallback.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.Unsupported.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.Unsupported.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.matrix-org.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-en-GB.matrix-org.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.Credentials-Entered.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.Credentials-Entered.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.OIDC-Fallback.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.OIDC-Fallback.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.Unsupported.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.Unsupported.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.matrix-org.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPad-pseudo.matrix-org.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.Credentials-Entered.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.Credentials-Entered.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.OIDC-Fallback.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.OIDC-Fallback.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.Unsupported.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.Unsupported.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.matrix-org.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-en-GB.matrix-org.png
(Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-pseudo.OIDC-Fallback.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-pseudo.OIDC-Fallback.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-pseudo.Unsupported.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-pseudo.Unsupported.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-pseudo.matrix-org.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_loginScreen-iPhone-16-pseudo.matrix-org.png
(Stored with Git LFS)
Binary file not shown.
173
UnitTests/Sources/LoginScreenViewModelTests.swift
Normal file
173
UnitTests/Sources/LoginScreenViewModelTests.swift
Normal 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.")
|
||||||
|
}
|
||||||
|
}
|
@ -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.")
|
|
||||||
}
|
|
||||||
}
|
|
@ -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!
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user