#40: Add basic AuthenticationService and missing UI tests.

* Add MockAuthenticationService and ServerSelectionUITests.
* Add tests covering the Authentication flow.
This commit is contained in:
Doug
2022-07-04 10:00:27 +01:00
committed by GitHub
parent 0a199ee611
commit 26d8affaa8
29 changed files with 641 additions and 203 deletions
+54 -6
View File
@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 51; objectVersion = 52;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -73,6 +73,7 @@
2FE4EEF780553B25A446BBFB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFA5FD06AAAC4AF544B594E /* AppDelegate.swift */; }; 2FE4EEF780553B25A446BBFB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFA5FD06AAAC4AF544B594E /* AppDelegate.swift */; };
30122AB3484AC6C3A7F6A717 /* ActivityIndicatorView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */; }; 30122AB3484AC6C3A7F6A717 /* ActivityIndicatorView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */; };
3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */; }; 3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */; };
32FC143630CE22A9E403370B /* MockAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */; };
33B4E59D408AE6E02323EE41 /* NoticeRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDA364DFFC3AC71C4771251 /* NoticeRoomMessage.swift */; }; 33B4E59D408AE6E02323EE41 /* NoticeRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDA364DFFC3AC71C4771251 /* NoticeRoomMessage.swift */; };
344AF4CBB6D8786214878642 /* NavigationRouterStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */; }; 344AF4CBB6D8786214878642 /* NavigationRouterStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */; };
34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; }; 34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; };
@@ -115,6 +116,7 @@
5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; };
53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */; }; 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */; };
541374590CA7E8318BD480FD /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; 541374590CA7E8318BD480FD /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; };
56DACDD379A86A1F5DEFE7BE /* AuthenticationServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */; };
56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; }; 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; };
59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; };
5B2C4C17888FC095ED6880B2 /* SplashViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */; }; 5B2C4C17888FC095ED6880B2 /* SplashViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */; };
@@ -133,6 +135,7 @@
684BDE198AE5AA1392288A73 /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32CE6D4FF64C9A3C18619224 /* SplashScreen.swift */; }; 684BDE198AE5AA1392288A73 /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32CE6D4FF64C9A3C18619224 /* SplashScreen.swift */; };
68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; };
69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; };
6A0E7551E0D1793245F34CDD /* ClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09A267106B9585D3D0CFC0D /* ClientError.swift */; };
6A367F3D7A437A79B7D9A31C /* FullscreenLoadingViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4112D04077F6709C5CA0A13E /* FullscreenLoadingViewPresenter.swift */; }; 6A367F3D7A437A79B7D9A31C /* FullscreenLoadingViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4112D04077F6709C5CA0A13E /* FullscreenLoadingViewPresenter.swift */; };
6AC1DC1EAD9F7568360DA1BA /* ServerSelectionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */; }; 6AC1DC1EAD9F7568360DA1BA /* ServerSelectionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */; };
6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; }; 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; };
@@ -151,6 +154,7 @@
7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; }; 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; };
77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */; }; 77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */; };
77E192BA943B90F9F310CA23 /* WeakDictionaryKeyReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FFCC48E7F701B6C24484593 /* WeakDictionaryKeyReference.swift */; }; 77E192BA943B90F9F310CA23 /* WeakDictionaryKeyReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FFCC48E7F701B6C24484593 /* WeakDictionaryKeyReference.swift */; };
77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */; };
78B71D53C1FC55FB7A9B75F0 /* RoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */; }; 78B71D53C1FC55FB7A9B75F0 /* RoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */; };
7963F98CDFDEAC75E072BD81 /* TextRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */; }; 7963F98CDFDEAC75E072BD81 /* TextRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */; };
79A6E08ADE6E7C460A8A17A5 /* UserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C37FB986891D90BEAA93EAE /* UserSessionStore.swift */; }; 79A6E08ADE6E7C460A8A17A5 /* UserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C37FB986891D90BEAA93EAE /* UserSessionStore.swift */; };
@@ -187,6 +191,7 @@
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; }; 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; };
9738F894DB1BD383BE05767A /* ElementSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1027BB9A852F445B7623897F /* ElementSettings.swift */; }; 9738F894DB1BD383BE05767A /* ElementSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1027BB9A852F445B7623897F /* ElementSettings.swift */; };
978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; }; 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; };
9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */; };
989029A28C9E2F828AD6658A /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; }; 989029A28C9E2F828AD6658A /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; };
992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; };
99ED42B8F8D6BFB1DBCF4C45 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 36B7FC232711031AA2B0D188 /* DTCoreText */; }; 99ED42B8F8D6BFB1DBCF4C45 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 36B7FC232711031AA2B0D188 /* DTCoreText */; };
@@ -214,6 +219,7 @@
A8EC7C9D886244DAE9433E37 /* SessionVerificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */; }; A8EC7C9D886244DAE9433E37 /* SessionVerificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */; };
AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9760103CF316DF68698BCFE6 /* LaunchScreen.storyboard */; }; AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9760103CF316DF68698BCFE6 /* LaunchScreen.storyboard */; };
ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; }; ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; };
ACF094CF3BF02DBFA6DFDE60 /* AuthenticationCoordinatorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */; };
B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */; }; B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */; };
B245583C63F8F90357B87FAE /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; B245583C63F8F90357B87FAE /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; };
B3357B00F1AA930E54F76609 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; B3357B00F1AA930E54F76609 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; };
@@ -253,6 +259,7 @@
D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; }; D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; };
D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */; }; D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */; };
D826154612415D2A3BB6EBF3 /* ListTableViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E854E7CF531DAC5CBEBDC75 /* ListTableViewAdapter.swift */; }; D826154612415D2A3BB6EBF3 /* ListTableViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E854E7CF531DAC5CBEBDC75 /* ListTableViewAdapter.swift */; };
D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */; };
D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */; }; D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */; };
D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */; }; D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */; };
DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CC95CD75B688E946438165 /* Coordinator.swift */; }; DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CC95CD75B688E946438165 /* Coordinator.swift */; };
@@ -268,10 +275,11 @@
EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */; }; EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */; };
EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; }; EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; };
EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1B52D0ABBA7091A991CAFE /* UserSessionStoreProtocol.swift */; }; EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1B52D0ABBA7091A991CAFE /* UserSessionStoreProtocol.swift */; };
EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F40F48279322E504153AB0D /* MockClientProxy.swift */; };
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; };
EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; };
EF99A92701E401C4CD5ADC50 /* SplashScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */; }; EF99A92701E401C4CD5ADC50 /* SplashScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */; };
F03E16ED043C62FED5A07AE0 /* MatrixEntitityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B81C8227BBEA95CCE86037 /* MatrixEntitityRegex.swift */; }; F03E16ED043C62FED5A07AE0 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B81C8227BBEA95CCE86037 /* MatrixEntityRegex.swift */; };
F040ABFEB0A2B142D948BA12 /* Untranslated.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = F75DF9500D69A3AAF8339E69 /* Untranslated.stringsdict */; }; F040ABFEB0A2B142D948BA12 /* Untranslated.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = F75DF9500D69A3AAF8339E69 /* Untranslated.stringsdict */; };
F2DD8661B5C0BA2BB526FA6C /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD51F9FDC91C231906D76C8 /* KeychainControllerProtocol.swift */; }; F2DD8661B5C0BA2BB526FA6C /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD51F9FDC91C231906D76C8 /* KeychainControllerProtocol.swift */; };
F4C3FEDB1B3A05376A1723A3 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4427F9E0571B4E6E048A2B /* KeychainController.swift */; }; F4C3FEDB1B3A05376A1723A3 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4427F9E0571B4E6E048A2B /* KeychainController.swift */; };
@@ -309,6 +317,7 @@
02A07FF019724B6ACEA73076 /* szl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = szl; path = szl.lproj/Localizable.strings; sourceTree = "<group>"; }; 02A07FF019724B6ACEA73076 /* szl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = szl; path = szl.lproj/Localizable.strings; sourceTree = "<group>"; };
04BBC9E08250EF92ADE89CFD /* sr-Latn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sr-Latn"; path = "sr-Latn.lproj/Localizable.strings"; sourceTree = "<group>"; }; 04BBC9E08250EF92ADE89CFD /* sr-Latn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sr-Latn"; path = "sr-Latn.lproj/Localizable.strings"; sourceTree = "<group>"; };
04E1273CC3BC3E471AF87BE5 /* UserIndicatorQueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorQueueTests.swift; sourceTree = "<group>"; }; 04E1273CC3BC3E471AF87BE5 /* UserIndicatorQueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorQueueTests.swift; sourceTree = "<group>"; };
054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionUITests.swift; sourceTree = "<group>"; };
057B747CF045D3C6C30EAB2C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 057B747CF045D3C6C30EAB2C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = "<group>"; }; 086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = "<group>"; };
08F64963396A6A23538EFCEC /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = is; path = is.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 08F64963396A6A23538EFCEC /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = is; path = is.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -392,6 +401,7 @@
3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = "<group>"; }; 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = "<group>"; };
3DD6E7C1D8B53F47789778CD /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = "<group>"; }; 3DD6E7C1D8B53F47789778CD /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = "<group>"; };
3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTaskService.swift; sourceTree = "<group>"; }; 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTaskService.swift; sourceTree = "<group>"; };
3F40F48279322E504153AB0D /* MockClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockClientProxy.swift; sourceTree = "<group>"; };
3F87116470221880017CF522 /* BuildSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildSettings.swift; sourceTree = "<group>"; }; 3F87116470221880017CF522 /* BuildSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildSettings.swift; sourceTree = "<group>"; };
3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorStore.swift; sourceTree = "<group>"; }; 3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorStore.swift; sourceTree = "<group>"; };
3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactory.swift; sourceTree = "<group>"; }; 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactory.swift; sourceTree = "<group>"; };
@@ -443,7 +453,9 @@
5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStoreProtocol.swift; sourceTree = "<group>"; }; 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStoreProtocol.swift; sourceTree = "<group>"; };
5CB7F9D6FC121204D59E18DF /* Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = "<group>"; }; 5CB7F9D6FC121204D59E18DF /* Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = "<group>"; };
5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = "<group>"; }; 5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = "<group>"; };
5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinatorUITests.swift; sourceTree = "<group>"; };
5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorTests.swift; sourceTree = "<group>"; }; 5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorTests.swift; sourceTree = "<group>"; };
5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProtocol.swift; sourceTree = "<group>"; };
5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = "<group>"; }; 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = "<group>"; };
5F77E8010D41AA3F5F9A1FCA /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = "<group>"; }; 5F77E8010D41AA3F5F9A1FCA /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = "<group>"; };
@@ -551,6 +563,7 @@
A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionModels.swift; sourceTree = "<group>"; }; A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionModels.swift; sourceTree = "<group>"; };
A436057DBEA1A23CA8CB1FD7 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = "<group>"; }; A436057DBEA1A23CA8CB1FD7 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = "<group>"; };
A443FAE2EE820A5790C35C8D /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = "<group>"; }; A443FAE2EE820A5790C35C8D /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = "<group>"; };
A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserSession.swift; sourceTree = "<group>"; };
A64F0DB78E0AC23C91AD89EF /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = mk.lproj/Localizable.strings; sourceTree = "<group>"; }; A64F0DB78E0AC23C91AD89EF /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = mk.lproj/Localizable.strings; sourceTree = "<group>"; };
A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; }; A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; };
A72232816DCE2B76D48E1367 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = "<group>"; }; A72232816DCE2B76D48E1367 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -624,6 +637,7 @@
CF47564C584F614B7287F3EB /* RootRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouter.swift; sourceTree = "<group>"; }; CF47564C584F614B7287F3EB /* RootRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouter.swift; sourceTree = "<group>"; };
CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementNavigationController.swift; sourceTree = "<group>"; }; CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementNavigationController.swift; sourceTree = "<group>"; };
CF847B3C1873B8E81CEE7FAC /* SplashScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenViewModel.swift; sourceTree = "<group>"; }; CF847B3C1873B8E81CEE7FAC /* SplashScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenViewModel.swift; sourceTree = "<group>"; };
D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = "<group>"; };
D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = "<group>"; }; D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = "<group>"; };
D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionVerificationControllerProxy.swift; sourceTree = "<group>"; }; D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionVerificationControllerProxy.swift; sourceTree = "<group>"; };
D29EBCBFEC6FD0941749404D /* NavigationRouterStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStore.swift; sourceTree = "<group>"; }; D29EBCBFEC6FD0941749404D /* NavigationRouterStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStore.swift; sourceTree = "<group>"; };
@@ -634,6 +648,7 @@
D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = "<group>"; }; D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = "<group>"; };
D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = "<group>"; }; D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = "<group>"; };
D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = "<group>"; }; D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = "<group>"; };
DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationService.swift; sourceTree = "<group>"; };
DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateCoordinator.swift; sourceTree = "<group>"; }; DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateCoordinator.swift; sourceTree = "<group>"; };
DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = "<group>"; }; DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = "<group>"; };
DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenModels.swift; sourceTree = "<group>"; }; DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenModels.swift; sourceTree = "<group>"; };
@@ -670,12 +685,13 @@
F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBugReportService.swift; sourceTree = "<group>"; }; F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBugReportService.swift; sourceTree = "<group>"; };
F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
F2D58333B377888012740101 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = "<group>"; }; F2D58333B377888012740101 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = "<group>"; };
F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetector.swift; sourceTree = "<group>"; }; F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetector.swift; sourceTree = "<group>"; };
F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = "<group>"; }; F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = "<group>"; };
F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = "<group>"; }; F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = "<group>"; };
F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = "<group>"; }; F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = "<group>"; };
F7B81C8227BBEA95CCE86037 /* MatrixEntitityRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntitityRegex.swift; sourceTree = "<group>"; }; F7B81C8227BBEA95CCE86037 /* MatrixEntityRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegex.swift; sourceTree = "<group>"; };
F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = "<group>"; }; F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = "<group>"; };
FA154570F693D93513E584C1 /* RoomMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactory.swift; sourceTree = "<group>"; }; FA154570F693D93513E584C1 /* RoomMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactory.swift; sourceTree = "<group>"; };
FAB10E673916D2B8D21FD197 /* TemplateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModels.swift; sourceTree = "<group>"; }; FAB10E673916D2B8D21FD197 /* TemplateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModels.swift; sourceTree = "<group>"; };
@@ -753,6 +769,7 @@
0787F81684E503024BD0C051 /* Services */ = { 0787F81684E503024BD0C051 /* Services */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
AAFDD509929A0CCF8BCE51EB /* Authentication */,
EBBEB5471737E9D116DF4738 /* Background */, EBBEB5471737E9D116DF4738 /* Background */,
0ED3F5C21537519389C07644 /* BugReport */, 0ED3F5C21537519389C07644 /* BugReport */,
8039515BAA53B7C3275AC64A /* Client */, 8039515BAA53B7C3275AC64A /* Client */,
@@ -825,6 +842,13 @@
path = Resources; path = Resources;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
3180C73BA7B8F5F7447C99B0 /* React */ = {
isa = PBXGroup;
children = (
);
path = React;
sourceTree = "<group>";
};
328DD5DA1281F758B72006C7 /* Views */ = { 328DD5DA1281F758B72006C7 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1159,8 +1183,10 @@
8039515BAA53B7C3275AC64A /* Client */ = { 8039515BAA53B7C3275AC64A /* Client */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D09A267106B9585D3D0CFC0D /* ClientError.swift */,
18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */, 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */,
6033779EB37259F27F938937 /* ClientProxyProtocol.swift */, 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */,
3F40F48279322E504153AB0D /* MockClientProxy.swift */,
); );
path = Client; path = Client;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1176,6 +1202,7 @@
82D5AD3EAE3A5C1068A44A88 /* Session */ = { 82D5AD3EAE3A5C1068A44A88 /* Session */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */,
6E5E9C044BEB7C70B1378E91 /* UserSession.swift */, 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */,
5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */, 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */,
); );
@@ -1232,10 +1259,12 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7D0CBC76C80E04345E11F2DB /* Application.swift */, 7D0CBC76C80E04345E11F2DB /* Application.swift */,
5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */,
C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */,
4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */, 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */,
1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */, 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */,
086B997409328F091EBA43CE /* RoomScreenUITests.swift */, 086B997409328F091EBA43CE /* RoomScreenUITests.swift */,
054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */,
6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */, 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */,
E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */, E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */,
325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */, 325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */,
@@ -1339,6 +1368,16 @@
path = UnitTests; path = UnitTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
AAFDD509929A0CCF8BCE51EB /* Authentication */ = {
isa = PBXGroup;
children = (
F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */,
5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */,
DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */,
);
path = Authentication;
sourceTree = "<group>";
};
AD5FCF9340D670C526AD17E4 /* UI */ = { AD5FCF9340D670C526AD17E4 /* UI */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1404,7 +1443,7 @@
CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */, CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */,
1027BB9A852F445B7623897F /* ElementSettings.swift */, 1027BB9A852F445B7623897F /* ElementSettings.swift */,
12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */, 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */,
F7B81C8227BBEA95CCE86037 /* MatrixEntitityRegex.swift */, F7B81C8227BBEA95CCE86037 /* MatrixEntityRegex.swift */,
44BBB96FAA2F0D53C507396B /* Extensions */, 44BBB96FAA2F0D53C507396B /* Extensions */,
8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */, 8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */,
06501F0E978B2D5C92771DC7 /* Logging */, 06501F0E978B2D5C92771DC7 /* Logging */,
@@ -1454,6 +1493,7 @@
E74CD7681375AD2EAA34D66B /* Authentication */, E74CD7681375AD2EAA34D66B /* Authentication */,
4009BE2E791C16AC6EE39A7E /* BugReport */, 4009BE2E791C16AC6EE39A7E /* BugReport */,
B53CA9BECD3F97805E1432D0 /* HomeScreen */, B53CA9BECD3F97805E1432D0 /* HomeScreen */,
3180C73BA7B8F5F7447C99B0 /* React */,
679E9837ECA8D6776079D16E /* RoomScreen */, 679E9837ECA8D6776079D16E /* RoomScreen */,
D958761758AA1110476DE6A3 /* SessionVerification */, D958761758AA1110476DE6A3 /* SessionVerification */,
70B74A432C241E56A7ACE610 /* Settings */, 70B74A432C241E56A7ACE610 /* Settings */,
@@ -1664,7 +1704,7 @@
}; };
}; };
buildConfigurationList = 7AE41FCCF9D1352E2770D1F9 /* Build configuration list for PBXProject "ElementX" */; buildConfigurationList = 7AE41FCCF9D1352E2770D1F9 /* Build configuration list for PBXProject "ElementX" */;
compatibilityVersion = "Xcode 10.0"; compatibilityVersion = "Xcode 11.0";
developmentRegion = en; developmentRegion = en;
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
knownRegions = ( knownRegions = (
@@ -1897,6 +1937,8 @@
A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */, A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */,
EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */, EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */,
B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */, B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */,
9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */,
56DACDD379A86A1F5DEFE7BE /* AuthenticationServiceProtocol.swift in Sources */,
E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */, E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */,
6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */, 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */,
CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */, CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */,
@@ -1911,6 +1953,7 @@
187E18F21EF4DA244E436E58 /* BugReportViewModelProtocol.swift in Sources */, 187E18F21EF4DA244E436E58 /* BugReportViewModelProtocol.swift in Sources */,
05776B005C57E92582F0CF08 /* BuildSettings.swift in Sources */, 05776B005C57E92582F0CF08 /* BuildSettings.swift in Sources */,
E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */, E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */,
6A0E7551E0D1793245F34CDD /* ClientError.swift in Sources */,
1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */, 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */,
24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */, 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */,
DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */, DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */,
@@ -1954,7 +1997,7 @@
2F30EFEB7BD39242D1AD96F3 /* LoginViewModelProtocol.swift in Sources */, 2F30EFEB7BD39242D1AD96F3 /* LoginViewModelProtocol.swift in Sources */,
B94368839BDB69172E28E245 /* MXLog.swift in Sources */, B94368839BDB69172E28E245 /* MXLog.swift in Sources */,
BCC3EDB7AD0902797CB4BBC2 /* MXLogger.m in Sources */, BCC3EDB7AD0902797CB4BBC2 /* MXLogger.m in Sources */,
F03E16ED043C62FED5A07AE0 /* MatrixEntitityRegex.swift in Sources */, F03E16ED043C62FED5A07AE0 /* MatrixEntityRegex.swift in Sources */,
EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */, EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */,
7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */, 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */,
62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */, 62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */,
@@ -1963,13 +2006,16 @@
A5EC21A071F58FC1229C20D0 /* MemberDetailsProviderProtocol.swift in Sources */, A5EC21A071F58FC1229C20D0 /* MemberDetailsProviderProtocol.swift in Sources */,
24906A1E82D0046655958536 /* MessageComposer.swift in Sources */, 24906A1E82D0046655958536 /* MessageComposer.swift in Sources */,
072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */, 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */,
32FC143630CE22A9E403370B /* MockAuthenticationService.swift in Sources */,
28410F3DE89C2C44E4F75C92 /* MockBugReportService.swift in Sources */, 28410F3DE89C2C44E4F75C92 /* MockBugReportService.swift in Sources */,
EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */,
67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */, 67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */,
51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */, 51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */,
29AEE68A604940180AB9EBFF /* MockRoomSummary.swift in Sources */, 29AEE68A604940180AB9EBFF /* MockRoomSummary.swift in Sources */,
E81EEC1675F2371D12A880A3 /* MockRoomTimelineController.swift in Sources */, E81EEC1675F2371D12A880A3 /* MockRoomTimelineController.swift in Sources */,
9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */, 9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */,
D034A195A3494E38BF060485 /* MockSessionVerificationControllerProxy.swift in Sources */, D034A195A3494E38BF060485 /* MockSessionVerificationControllerProxy.swift in Sources */,
D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */,
4ED453A61AF45EBE18D8BC69 /* NavigationModule.swift in Sources */, 4ED453A61AF45EBE18D8BC69 /* NavigationModule.swift in Sources */,
22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */, 22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */,
12F70C493FB69F4D7E9A37EA /* NavigationRouterStore.swift in Sources */, 12F70C493FB69F4D7E9A37EA /* NavigationRouterStore.swift in Sources */,
@@ -2091,12 +2137,14 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7405B4824D45BA7C3D943E76 /* Application.swift in Sources */, 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */,
ACF094CF3BF02DBFA6DFDE60 /* AuthenticationCoordinatorUITests.swift in Sources */,
7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */, 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */,
499A26EB06C97E48C27A2DB9 /* BuildSettings.swift in Sources */, 499A26EB06C97E48C27A2DB9 /* BuildSettings.swift in Sources */,
94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */, 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */,
9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */, 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */,
5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */, 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */,
2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */, 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */,
77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */,
05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */, 05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */,
490E606044B18985055FF690 /* SettingsUITests.swift in Sources */, 490E606044B18985055FF690 /* SettingsUITests.swift in Sources */,
A00DFC1DD3567B1EDC9F8D16 /* SplashScreenUITests.swift in Sources */, A00DFC1DD3567B1EDC9F8D16 /* SplashScreenUITests.swift in Sources */,
+4 -17
View File
@@ -89,23 +89,15 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
// MARK: - AuthenticationCoordinatorDelegate // MARK: - AuthenticationCoordinatorDelegate
func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator) {
stateMachine.processEvent(.attemptedSignIn)
}
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) { func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) {
self.userSession = userSession self.userSession = userSession
remove(childCoordinator: authenticationCoordinator) remove(childCoordinator: authenticationCoordinator)
stateMachine.processEvent(.succeededSigningIn) stateMachine.processEvent(.succeededSigningIn)
} }
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didFailWithError error: AuthenticationCoordinatorError) {
stateMachine.processEvent(.failedSigningIn)
}
// MARK: - Private // MARK: - Private
// swiftlint:disable cyclomatic_complexity function_body_length // swiftlint:disable cyclomatic_complexity
private func setupStateMachine() { private func setupStateMachine() {
stateMachine.addTransitionHandler { [weak self] context in stateMachine.addTransitionHandler { [weak self] context in
guard let self = self else { return } guard let self = self else { return }
@@ -113,13 +105,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
switch (context.fromState, context.event, context.toState) { switch (context.fromState, context.event, context.toState) {
case (.initial, .startWithAuthentication, .signedOut): case (.initial, .startWithAuthentication, .signedOut):
self.startAuthentication() self.startAuthentication()
case (.signedOut, .attemptedSignIn, .signingIn): case (.signedOut, .succeededSigningIn, .homeScreen):
self.showLoadingIndicator()
case (.signingIn, .failedSigningIn, .signedOut):
self.hideLoadingIndicator()
self.showLoginErrorToast()
case (.signingIn, .succeededSigningIn, .homeScreen):
self.hideLoadingIndicator()
self.presentHomeScreen() self.presentHomeScreen()
case (.initial, .startWithExistingSession, .restoringSession): case (.initial, .startWithExistingSession, .restoringSession):
@@ -179,7 +165,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
} }
private func startAuthentication() { private func startAuthentication() {
let coordinator = AuthenticationCoordinator(userSessionStore: userSessionStore, let authenticationService = AuthenticationService(userSessionStore: userSessionStore)
let coordinator = AuthenticationCoordinator(authenticationService: authenticationService,
navigationRouter: navigationRouter) navigationRouter: navigationRouter)
coordinator.delegate = self coordinator.delegate = self
@@ -16,8 +16,6 @@ class AppCoordinatorStateMachine {
case initial case initial
/// Showing the login screen /// Showing the login screen
case signedOut case signedOut
/// Processing sign in request
case signingIn
/// Opening an existing session. /// Opening an existing session.
case restoringSession case restoringSession
/// Showing the home screen /// Showing the home screen
@@ -41,12 +39,8 @@ class AppCoordinatorStateMachine {
enum Event: EventType { enum Event: EventType {
/// Start the `AppCoordinator` by showing authentication. /// Start the `AppCoordinator` by showing authentication.
case startWithAuthentication case startWithAuthentication
/// A sign in request has been started
case attemptedSignIn
/// Signing in succeeded /// Signing in succeeded
case succeededSigningIn case succeededSigningIn
/// Signing in failed
case failedSigningIn
/// Start the `AppCoordinator` by restoring an existing account. /// Start the `AppCoordinator` by restoring an existing account.
case startWithExistingSession case startWithExistingSession
@@ -84,9 +78,7 @@ class AppCoordinatorStateMachine {
init() { init() {
stateMachine = StateMachine(state: .initial) { machine in stateMachine = StateMachine(state: .initial) { machine in
machine.addRoutes(event: .startWithAuthentication, transitions: [ .initial => .signedOut ]) machine.addRoutes(event: .startWithAuthentication, transitions: [ .initial => .signedOut ])
machine.addRoutes(event: .attemptedSignIn, transitions: [ .signedOut => .signingIn ]) machine.addRoutes(event: .succeededSigningIn, transitions: [ .signedOut => .homeScreen ])
machine.addRoutes(event: .succeededSigningIn, transitions: [ .signingIn => .homeScreen ])
machine.addRoutes(event: .failedSigningIn, transitions: [ .signingIn => .signedOut ])
machine.addRoutes(event: .startWithExistingSession, transitions: [ .initial => .restoringSession ]) machine.addRoutes(event: .startWithExistingSession, transitions: [ .initial => .restoringSession ])
machine.addRoutes(event: .succeededRestoringSession, transitions: [ .restoringSession => .homeScreen ]) machine.addRoutes(event: .succeededRestoringSession, transitions: [ .restoringSession => .homeScreen ])
@@ -31,4 +31,12 @@ extension String {
return string return string
} }
/// Whether or not the string is a Matrix user ID.
var isMatrixUserID: Bool {
let range = NSRange(location: 0, length: count)
let detector = try? NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive)
return detector?.numberOfMatches(in: self, range: range) ?? 0 == 1
}
} }
@@ -6,28 +6,18 @@
// Copyright © 2022 Element. All rights reserved. // Copyright © 2022 Element. All rights reserved.
// //
import Foundation import UIKit
import MatrixRustSDK import MatrixRustSDK
enum AuthenticationCoordinatorError: Error {
case failedLoggingIn
}
@MainActor @MainActor
protocol AuthenticationCoordinatorDelegate: AnyObject { protocol AuthenticationCoordinatorDelegate: AnyObject {
func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator)
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator,
didLoginWithSession userSession: UserSessionProtocol) didLoginWithSession userSession: UserSessionProtocol)
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator,
didFailWithError error: AuthenticationCoordinatorError)
} }
class AuthenticationCoordinator: Coordinator { class AuthenticationCoordinator: Coordinator, Presentable {
private let userSessionStore: UserSessionStoreProtocol private let authenticationService: AuthenticationServiceProtocol
private let navigationRouter: NavigationRouter private let navigationRouter: NavigationRouter
private(set) var clientProxy: ClientProxyProtocol? private(set) var clientProxy: ClientProxyProtocol?
@@ -35,9 +25,9 @@ class AuthenticationCoordinator: Coordinator {
weak var delegate: AuthenticationCoordinatorDelegate? weak var delegate: AuthenticationCoordinatorDelegate?
init(userSessionStore: UserSessionStoreProtocol, init(authenticationService: AuthenticationServiceProtocol,
navigationRouter: NavigationRouter) { navigationRouter: NavigationRouter) {
self.userSessionStore = userSessionStore self.authenticationService = authenticationService
self.navigationRouter = navigationRouter self.navigationRouter = navigationRouter
} }
@@ -45,6 +35,10 @@ class AuthenticationCoordinator: Coordinator {
showSplashScreen() showSplashScreen()
} }
func toPresentable() -> UIViewController {
navigationRouter.toPresentable()
}
// MARK: - Private // MARK: - Private
private func showSplashScreen() { private func showSplashScreen() {
@@ -67,28 +61,18 @@ class AuthenticationCoordinator: Coordinator {
} }
private func showLoginScreen() { private func showLoginScreen() {
let homeserver = LoginHomeserver(address: BuildSettings.defaultHomeserverURLString) let parameters = LoginCoordinatorParameters(authenticationService: authenticationService,
let parameters = LoginCoordinatorParameters(navigationRouter: navigationRouter, homeserver: homeserver) navigationRouter: navigationRouter)
let coordinator = LoginCoordinator(parameters: parameters) let coordinator = LoginCoordinator(parameters: parameters)
coordinator.callback = { [weak self, weak coordinator] action in coordinator.callback = { [weak self, weak coordinator] action in
guard let self = self, let coordinator = coordinator else { guard let self = self, let coordinator = coordinator else { return }
return
}
switch action { switch action {
case .login(let username, let password): case .signedIn(let userSession):
Task { self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
switch await self.login(username: username, password: password) { self.remove(childCoordinator: coordinator)
case .success(let userSession): self.navigationRouter.dismissModule()
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
self.remove(childCoordinator: coordinator)
self.navigationRouter.dismissModule()
case .failure(let error):
self.delegate?.authenticationCoordinator(self, didFailWithError: error)
MXLog.error("Failed logging in user with error: \(error)")
}
}
case .continueWithOIDC: case .continueWithOIDC:
break break
} }
@@ -101,38 +85,4 @@ class AuthenticationCoordinator: Coordinator {
self?.remove(childCoordinator: coordinator) self?.remove(childCoordinator: coordinator)
} }
} }
private func login(username: String, password: String) async -> Result<UserSession, AuthenticationCoordinatorError> {
Benchmark.startTrackingForIdentifier("Login", message: "Started new login")
delegate?.authenticationCoordinatorDidStartLoading(self)
let basePath = userSessionStore.baseDirectoryPath(for: username)
let builder = ClientBuilder()
.basePath(path: basePath)
.username(username: username)
let loginTask: Task<Client, Error> = Task.detached {
let client = try builder.build()
try client.login(username: username, password: password)
return client
}
switch await loginTask.result {
case .success(let client):
return await userSession(for: client)
case .failure(let error):
MXLog.error("Failed logging in with error: \(error)")
return .failure(.failedLoggingIn)
}
}
private func userSession(for client: Client) async -> Result<UserSession, AuthenticationCoordinatorError> {
switch await userSessionStore.userSession(for: client) {
case .success(let clientProxy):
return .success(clientProxy)
case .failure:
return .failure(.failedLoggingIn)
}
}
} }
@@ -18,26 +18,17 @@ import SwiftUI
import MatrixRustSDK import MatrixRustSDK
struct LoginCoordinatorParameters { struct LoginCoordinatorParameters {
/// The service used to authenticate the user.
let authenticationService: AuthenticationServiceProtocol
/// The navigation router used to present the server selection screen.
let navigationRouter: NavigationRouterType let navigationRouter: NavigationRouterType
/// The homeserver to be shown initially.
let homeserver: LoginHomeserver
} }
enum LoginCoordinatorAction: CustomStringConvertible { enum LoginCoordinatorAction {
/// Login with the associated username and password. /// Login was successful.
case login(username: String, password: String) case signedIn(UserSessionProtocol)
/// Continue using OIDC. /// Continue using OIDC.
case continueWithOIDC case continueWithOIDC
/// A string representation of the action, ignoring any associated values that could leak PII.
var description: String {
switch self {
case .login:
return "login"
case .continueWithOIDC:
return "continueWithOIDC"
}
}
} }
final class LoginCoordinator: Coordinator, Presentable { final class LoginCoordinator: Coordinator, Presentable {
@@ -56,6 +47,7 @@ final class LoginCoordinator: Coordinator, Presentable {
} }
} }
private var authenticationService: AuthenticationServiceProtocol { parameters.authenticationService }
private var navigationRouter: NavigationRouterType { parameters.navigationRouter } private var navigationRouter: NavigationRouterType { parameters.navigationRouter }
private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var indicatorPresenter: UserIndicatorTypePresenterProtocol
private var activityIndicator: UserIndicator? private var activityIndicator: UserIndicator?
@@ -71,7 +63,7 @@ final class LoginCoordinator: Coordinator, Presentable {
init(parameters: LoginCoordinatorParameters) { init(parameters: LoginCoordinatorParameters) {
self.parameters = parameters self.parameters = parameters
let viewModel = LoginViewModel(homeserver: parameters.homeserver) let viewModel = LoginViewModel(homeserver: parameters.authenticationService.homeserver)
loginViewModel = viewModel loginViewModel = viewModel
let view = LoginScreen(context: viewModel.context) let view = LoginScreen(context: viewModel.context)
@@ -135,56 +127,68 @@ final class LoginCoordinator: Coordinator, Presentable {
} }
/// Processes an error to either update the flow or display it to the user. /// Processes an error to either update the flow or display it to the user.
private func handleError(_ error: Error) { private func handleError(_ error: AuthenticationServiceError) {
loginViewModel.displayError(.alert(error.localizedDescription)) switch error {
case .invalidCredentials:
loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginParam))
case .accountDeactivated:
loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount))
default:
loginViewModel.displayError(.alert(ElementL10n.unknownError))
}
} }
/// Requests the authentication coordinator to log in using the specified credentials. /// Requests the authentication coordinator to log in using the specified credentials.
private func login(username: String, password: String) { private func login(username: String, password: String) {
var username = loginViewModel.context.username startLoading(isInteractionBlocking: true)
if !isMXID(username: username) { Task {
let homeserver = loginViewModel.context.viewState.homeserver switch await authenticationService.login(username: username, password: password) {
username = "@\(username):\(homeserver.address)" case .success(let userSession):
callback?(.signedIn(userSession))
stopLoading()
case .failure(let error):
stopLoading()
handleError(error)
}
} }
callback?(.login(username: username, password: password))
} }
/// Parses the specified username and looks up the homeserver when a Matrix ID is entered. /// Parses the specified username and looks up the homeserver when a Matrix ID is entered.
private func parseUsername(_ username: String) { private func parseUsername(_ username: String) {
guard isMXID(username: username) else { return } guard username.isMatrixUserID else { return }
let domain = String(username.split(separator: ":")[1]) let homeserverDomain = String(username.split(separator: ":")[1])
let homeserver = LoginHomeserver(address: domain) startLoading(isInteractionBlocking: false)
updateViewModel(homeserver: homeserver)
indicateSuccess()
}
/// Checks whether the specified username is a Matrix ID or not.
private func isMXID(username: String) -> Bool {
let range = NSRange(location: 0, length: username.count)
let detector = try? NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive) Task {
return detector?.numberOfMatches(in: username, range: range) ?? 0 > 0 switch await authenticationService.startLogin(for: homeserverDomain) {
case .success:
updateViewModel()
stopLoading()
case .failure(let error):
stopLoading()
handleError(error)
}
}
} }
/// Updates the view model with a different homeserver. /// Updates the view model with a different homeserver.
private func updateViewModel(homeserver: LoginHomeserver) { private func updateViewModel() {
loginViewModel.update(homeserver: homeserver) loginViewModel.update(homeserver: authenticationService.homeserver)
indicateSuccess() indicateSuccess()
} }
/// Presents the server selection screen as a modal. /// Presents the server selection screen as a modal.
private func presentServerSelectionScreen() { private func presentServerSelectionScreen() {
MXLog.debug("[LoginCoordinator] presentServerSelectionScreen") MXLog.debug("[LoginCoordinator] presentServerSelectionScreen")
let parameters = ServerSelectionCoordinatorParameters(homeserver: loginViewModel.context.viewState.homeserver, let parameters = ServerSelectionCoordinatorParameters(authenticationService: authenticationService,
hasModalPresentation: true) hasModalPresentation: true)
let coordinator = ServerSelectionCoordinator(parameters: parameters) let coordinator = ServerSelectionCoordinator(parameters: parameters)
coordinator.callback = { [weak self, weak coordinator] result in coordinator.callback = { [weak self, weak coordinator] action in
guard let self = self, let coordinator = coordinator else { return } guard let self = self, let coordinator = coordinator else { return }
self.serverSelectionCoordinator(coordinator, didCompleteWith: result) self.serverSelectionCoordinator(coordinator, didCompleteWith: action)
} }
coordinator.start() coordinator.start()
@@ -198,10 +202,10 @@ final class LoginCoordinator: Coordinator, Presentable {
/// Handles the result from the server selection modal, dismissing it after updating the view. /// Handles the result from the server selection modal, dismissing it after updating the view.
private func serverSelectionCoordinator(_ coordinator: ServerSelectionCoordinator, private func serverSelectionCoordinator(_ coordinator: ServerSelectionCoordinator,
didCompleteWith result: ServerSelectionCoordinatorResult) { didCompleteWith action: ServerSelectionCoordinatorAction) {
navigationRouter.dismissModule(animated: true) { [weak self] in navigationRouter.dismissModule(animated: true) { [weak self] in
if case let .selected(homeserver) = result { if action == .updated {
self?.updateViewModel(homeserver: homeserver) self?.updateViewModel()
} }
self?.remove(childCoordinator: coordinator) self?.remove(childCoordinator: coordinator)
@@ -58,6 +58,7 @@ struct LoginServerInfoSection: View {
.padding(.vertical, 2) .padding(.vertical, 2)
} }
.buttonStyle(.elementGhost()) .buttonStyle(.elementGhost())
.accessibilityIdentifier("editServerButton")
} }
} }
} }
@@ -17,14 +17,14 @@
import SwiftUI import SwiftUI
struct ServerSelectionCoordinatorParameters { struct ServerSelectionCoordinatorParameters {
/// The homeserver to be shown initially. /// The service used to authenticate the user.
let homeserver: LoginHomeserver let authenticationService: AuthenticationServiceProtocol
/// Whether the screen is presented modally or within a navigation stack. /// Whether the screen is presented modally or within a navigation stack.
let hasModalPresentation: Bool let hasModalPresentation: Bool
} }
enum ServerSelectionCoordinatorResult { enum ServerSelectionCoordinatorAction {
case selected(LoginHomeserver) case updated
case dismiss case dismiss
} }
@@ -38,6 +38,7 @@ final class ServerSelectionCoordinator: Coordinator, Presentable {
private let serverSelectionHostingController: UIViewController private let serverSelectionHostingController: UIViewController
private var serverSelectionViewModel: ServerSelectionViewModelProtocol private var serverSelectionViewModel: ServerSelectionViewModelProtocol
private var authenticationService: AuthenticationServiceProtocol { parameters.authenticationService }
private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var indicatorPresenter: UserIndicatorTypePresenterProtocol
private var loadingIndicator: UserIndicator? private var loadingIndicator: UserIndicator?
@@ -45,14 +46,14 @@ final class ServerSelectionCoordinator: Coordinator, Presentable {
// Must be used only internally // Must be used only internally
var childCoordinators: [Coordinator] = [] var childCoordinators: [Coordinator] = []
var callback: (@MainActor (ServerSelectionCoordinatorResult) -> Void)? var callback: (@MainActor (ServerSelectionCoordinatorAction) -> Void)?
// MARK: - Setup // MARK: - Setup
init(parameters: ServerSelectionCoordinatorParameters) { init(parameters: ServerSelectionCoordinatorParameters) {
self.parameters = parameters self.parameters = parameters
let viewModel = ServerSelectionViewModel(homeserverAddress: parameters.homeserver.address, let viewModel = ServerSelectionViewModel(homeserverAddress: parameters.authenticationService.homeserver.address,
hasModalPresentation: parameters.hasModalPresentation) hasModalPresentation: parameters.hasModalPresentation)
let view = ServerSelectionScreen(context: viewModel.context) let view = ServerSelectionScreen(context: viewModel.context)
serverSelectionViewModel = viewModel serverSelectionViewModel = viewModel
@@ -66,11 +67,11 @@ final class ServerSelectionCoordinator: Coordinator, Presentable {
func start() { func start() {
MXLog.debug("[ServerSelectionCoordinator] did start.") MXLog.debug("[ServerSelectionCoordinator] did start.")
serverSelectionViewModel.callback = { [weak self] result in serverSelectionViewModel.callback = { [weak self] action in
guard let self = self else { return } guard let self = self else { return }
MXLog.debug("[ServerSelectionCoordinator] ServerSelectionViewModel did complete with result: \(result).") MXLog.debug("[ServerSelectionCoordinator] ServerSelectionViewModel did callback with action: \(action).")
switch result { switch action {
case .confirm(let homeserverAddress): case .confirm(let homeserverAddress):
self.useHomeserver(homeserverAddress) self.useHomeserver(homeserverAddress)
case .dismiss: case .dismiss:
@@ -102,9 +103,25 @@ final class ServerSelectionCoordinator: Coordinator, Presentable {
private func useHomeserver(_ homeserverAddress: String) { private func useHomeserver(_ homeserverAddress: String) {
startLoading() startLoading()
let homeserverAddress = LoginHomeserver.sanitized(homeserverAddress) Task {
switch await authenticationService.startLogin(for: homeserverAddress) {
stopLoading() case .success:
callback?(.selected(LoginHomeserver(address: homeserverAddress))) callback?(.updated)
stopLoading()
case .failure(let error):
stopLoading()
handleError(error)
}
}
}
/// Processes an error to either update the flow or display it to the user.
private func handleError(_ error: AuthenticationServiceError) {
switch error {
case .invalidServer:
serverSelectionViewModel.displayError(.footerMessage(ElementL10n.loginErrorHomeserverNotFound))
default:
serverSelectionViewModel.displayError(.footerMessage(ElementL10n.unknownError))
}
} }
} }
@@ -18,7 +18,7 @@ import Foundation
// MARK: View model // MARK: View model
enum ServerSelectionViewModelResult { enum ServerSelectionViewModelAction {
/// The user would like to use the homeserver at the given address. /// The user would like to use the homeserver at the given address.
case confirm(homeserverAddress: String) case confirm(homeserverAddress: String)
/// Dismiss the view without using the entered address. /// Dismiss the view without using the entered address.
@@ -26,7 +26,7 @@ class ServerSelectionViewModel: ServerSelectionViewModelType, ServerSelectionVie
// MARK: Public // MARK: Public
var callback: (@MainActor (ServerSelectionViewModelResult) -> Void)? var callback: (@MainActor (ServerSelectionViewModelAction) -> Void)?
// MARK: - Setup // MARK: - Setup
@@ -19,7 +19,7 @@ import Foundation
@MainActor @MainActor
protocol ServerSelectionViewModelProtocol { protocol ServerSelectionViewModelProtocol {
var callback: (@MainActor (ServerSelectionViewModelResult) -> Void)? { get set } var callback: (@MainActor (ServerSelectionViewModelAction) -> Void)? { get set }
var context: ServerSelectionViewModelType.Context { get } var context: ServerSelectionViewModelType.Context { get }
/// Displays an error to the user. /// Displays an error to the user.
@@ -0,0 +1,82 @@
//
// AuthenticationService.swift
// ElementX
//
// Created by Doug on 29/06/2022.
// Copyright © 2022 Element. All rights reserved.
//
import Foundation
import MatrixRustSDK
class AuthenticationService: AuthenticationServiceProtocol {
// MARK: - Properties
// MARK: Private
private(set) var homeserver: LoginHomeserver = LoginHomeserver(address: BuildSettings.defaultHomeserverURLString)
private let userSessionStore: UserSessionStoreProtocol
// MARK: - Setup
init(userSessionStore: UserSessionStoreProtocol) {
self.userSessionStore = userSessionStore
}
// MARK: - Public
func startLogin(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
homeserver = LoginHomeserver(address: homeserverAddress)
return .success(())
}
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
Benchmark.startTrackingForIdentifier("Login", message: "Started new login")
// Workaround whilst the SDK requires a full MXID.
let username = username.isMatrixUserID ? username : "@\(username):\(homeserver.address)"
let basePath = userSessionStore.baseDirectoryPath(for: username)
let builder = ClientBuilder()
.basePath(path: basePath)
.username(username: username)
let loginTask: Task<Client, Error> = Task.detached {
let client = try builder.build()
try client.login(username: username, password: password)
return client
}
switch await loginTask.result {
case .success(let client):
Benchmark.endTrackingForIdentifier("Login", message: "Finished login")
return await userSession(for: client)
case .failure(let error):
Benchmark.endTrackingForIdentifier("Login", message: "Login failed")
MXLog.error("Failed logging in with error: \(error)")
guard let error = error as? ClientError else { return .failure(.failedLoggingIn) }
switch error.code {
case .forbidden:
return .failure(.invalidCredentials)
case .userDeactivated:
return .failure(.accountDeactivated)
default:
return .failure(.failedLoggingIn)
}
}
}
// MARK: - Private
private func userSession(for client: Client) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
switch await userSessionStore.userSession(for: client) {
case .success(let clientProxy):
return .success(clientProxy)
case .failure:
return .failure(.failedLoggingIn)
}
}
}
@@ -0,0 +1,26 @@
//
// AuthenticationServiceProtocol.swift
// ElementX
//
// Created by Doug on 29/06/2022.
// Copyright © 2022 Element. All rights reserved.
//
import Foundation
enum AuthenticationServiceError: Error {
case invalidServer
case invalidCredentials
case accountDeactivated
case failedLoggingIn
}
@MainActor
protocol AuthenticationServiceProtocol {
var homeserver: LoginHomeserver { get }
/// Sets up the service for login on the specified homeserver address.
func startLogin(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError>
/// Performs a password login using the current homeserver.
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError>
}
@@ -0,0 +1,45 @@
//
// MockAuthenticationService.swift
// ElementX
//
// Created by Doug on 29/06/2022.
// Copyright © 2022 Element. All rights reserved.
//
import Foundation
class MockAuthenticationService: AuthenticationServiceProtocol {
let validCredentials = (username: "alice", password: "12345678")
private(set) var homeserver: LoginHomeserver = .mockMatrixDotOrg
func startLogin(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
// Map the address to the mock homeservers
if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) {
homeserver = .mockMatrixDotOrg
return .success(())
} else if LoginHomeserver.mockOIDC.address.contains(homeserverAddress) {
homeserver = .mockOIDC
return .success(())
} else if LoginHomeserver.mockBasicServer.address.contains(homeserverAddress) {
homeserver = .mockBasicServer
return .success(())
} else if LoginHomeserver.mockUnsupported.address.contains(homeserverAddress) {
homeserver = .mockUnsupported
return .success(())
} else {
// Otherwise fail with an invalid server.
return .failure(.invalidServer)
}
}
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
// Login only succeeds if the username and password match the valid credentials property
guard username == validCredentials.username, password == validCredentials.password else {
return .failure(.invalidCredentials)
}
let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: username),
mediaProvider: MockMediaProvider())
return .success(userSession)
}
}
@@ -0,0 +1,30 @@
//
// ClientError.swift
// ElementX
//
// Created by Doug on 30/06/2022.
// Copyright © 2022 Element. All rights reserved.
//
import Foundation
import MatrixRustSDK
enum MatrixErrorCode: String, CaseIterable {
case unknown = "M_UNKNOWN"
case userDeactivated = "M_USER_DEACTIVATED"
case forbidden = "M_FORBIDDEN"
}
extension ClientError {
var code: MatrixErrorCode {
guard case let .Generic(message) = self else { return .unknown }
for code in MatrixErrorCode.allCases {
if message.contains(code.rawValue) {
return code
}
}
return .unknown
}
}
@@ -19,6 +19,7 @@ enum ClientProxyError: Error {
case failedRetrievingAvatarURL case failedRetrievingAvatarURL
case failedRetrievingDisplayName case failedRetrievingDisplayName
case failedRetrievingSessionVerificationController case failedRetrievingSessionVerificationController
case failedLoadingMedia
} }
protocol ClientProxyProtocol { protocol ClientProxyProtocol {
@@ -0,0 +1,39 @@
//
// MockClientProxy.swift
// ElementX
//
// Created by Doug on 29/06/2022.
// Copyright © 2022 Element. All rights reserved.
//
import Combine
import MatrixRustSDK
struct MockClientProxy: ClientProxyProtocol {
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
let userIdentifier: String
let rooms = [RoomProxy]()
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
.failure(.failedRetrievingDisplayName)
}
func loadUserAvatarURLString() async -> Result<String, ClientProxyError> {
.failure(.failedRetrievingAvatarURL)
}
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource {
MatrixRustSDK.mediaSourceFromUrl(url: urlString)
}
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data {
throw ClientProxyError.failedLoadingMedia
}
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
.failure(.failedRetrievingSessionVerificationController)
}
}
@@ -0,0 +1,17 @@
//
// MockUserSession.swift
// ElementX
//
// Created by Doug on 29/06/2022.
// Copyright © 2022 Element. All rights reserved.
//
import Combine
struct MockUserSession: UserSessionProtocol {
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
let sessionVerificationController: SessionVerificationControllerProxyProtocol? = nil
let clientProxy: ClientProxyProtocol
let mediaProvider: MediaProviderProtocol
}
@@ -98,8 +98,6 @@ class UserSessionStore: UserSessionStoreProtocol {
} }
private func setupProxyForClient(_ client: Client) async -> Result<ClientProxyProtocol, UserSessionStoreError> { private func setupProxyForClient(_ client: Client) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
Benchmark.endTrackingForIdentifier("Login", message: "Finished login")
do { do {
let accessToken = try client.restoreToken() let accessToken = try client.restoreToken()
let userId = try client.userId() let userId = try client.userId()
@@ -10,8 +10,9 @@ import Foundation
enum UITestScreenIdentifier: String { enum UITestScreenIdentifier: String {
case login case login
case loginOIDC case serverSelection
case loginUnsupported case serverSelectionNonModal
case authenticationFlow
case simpleRegular case simpleRegular
case simpleUpgrade case simpleUpgrade
case settings case settings
+30 -20
View File
@@ -11,28 +11,37 @@ import SwiftUI
class UITestsAppCoordinator: Coordinator { class UITestsAppCoordinator: Coordinator {
private let window: UIWindow private let window: UIWindow
private let mainNavigationController: UINavigationController private let mainNavigationController: ElementNavigationController
private let navigationRouter: NavigationRouter
private var hostingController: UIViewController?
var childCoordinators: [Coordinator] = [] var childCoordinators: [Coordinator] = []
init() { init() {
mainNavigationController = UINavigationController() mainNavigationController = ElementNavigationController()
navigationRouter = NavigationRouter(navigationController: mainNavigationController)
window = UIWindow(frame: UIScreen.main.bounds) window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = mainNavigationController window.rootViewController = mainNavigationController
window.tintColor = .element.accent window.tintColor = .element.accent
UIView.setAnimationsEnabled(false)
let screens = mockScreens() let screens = mockScreens()
let rootView = UITestsRootView(mockScreens: screens) { id in let rootView = UITestsRootView(mockScreens: screens) { id in
guard let screen = screens.first(where: { $0.id == id }) else { guard let screen = screens.first(where: { $0.id == id }) else {
fatalError() fatalError()
} }
screen.coordinator.start() screen.coordinator.start()
self.navigationRouter.setRootModule(screen.coordinator)
self.mainNavigationController.pushViewController(screen.coordinator.toPresentable(), animated: true)
} }
mainNavigationController.setViewControllers([UIHostingController(rootView: rootView)], animated: false) let hostingController = UIHostingController(rootView: rootView)
self.hostingController = hostingController
mainNavigationController.setViewControllers([hostingController], animated: false)
} }
func start() { func start() {
@@ -40,34 +49,34 @@ class UITestsAppCoordinator: Coordinator {
} }
private func mockScreens() -> [MockScreen] { private func mockScreens() -> [MockScreen] {
UITestScreenIdentifier.allCases.map { MockScreen(id: $0) } UITestScreenIdentifier.allCases.map { MockScreen(id: $0, navigationRouter: navigationRouter) }
} }
} }
@MainActor @MainActor
class MockScreen: Identifiable { class MockScreen: Identifiable {
let id: UITestScreenIdentifier let id: UITestScreenIdentifier
let navigationRouter: NavigationRouter
lazy var coordinator: Coordinator & Presentable = { lazy var coordinator: Coordinator & Presentable = {
switch id { switch id {
case .login: case .login:
let router = NavigationRouter(navigationController: ElementNavigationController()) return LoginCoordinator(parameters: .init(authenticationService: MockAuthenticationService(),
return LoginCoordinator(parameters: .init(navigationRouter: router, navigationRouter: navigationRouter))
homeserver: .mockMatrixDotOrg)) case .serverSelection:
case .loginOIDC: return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationService(),
let router = NavigationRouter(navigationController: ElementNavigationController()) hasModalPresentation: true))
return LoginCoordinator(parameters: .init(navigationRouter: router, case .serverSelectionNonModal:
homeserver: .mockOIDC)) return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationService(),
case .loginUnsupported: hasModalPresentation: false))
let router = NavigationRouter(navigationController: ElementNavigationController()) case .authenticationFlow:
return LoginCoordinator(parameters: .init(navigationRouter: router, return AuthenticationCoordinator(authenticationService: MockAuthenticationService(),
homeserver: .mockUnsupported)) navigationRouter: navigationRouter)
case .simpleRegular: case .simpleRegular:
return TemplateCoordinator(parameters: .init(promptType: .regular)) return TemplateCoordinator(parameters: .init(promptType: .regular))
case .simpleUpgrade: case .simpleUpgrade:
return TemplateCoordinator(parameters: .init(promptType: .upgrade)) return TemplateCoordinator(parameters: .init(promptType: .upgrade))
case .settings: case .settings:
let router = NavigationRouter(navigationController: ElementNavigationController()) return SettingsCoordinator(parameters: .init(navigationRouter: navigationRouter,
return SettingsCoordinator(parameters: .init(navigationRouter: router,
bugReportService: MockBugReportService())) bugReportService: MockBugReportService()))
case .bugReport: case .bugReport:
return BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(), return BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(),
@@ -95,7 +104,8 @@ class MockScreen: Identifiable {
} }
}() }()
init(id: UITestScreenIdentifier) { init(id: UITestScreenIdentifier, navigationRouter: NavigationRouter) {
self.id = id self.id = id
self.navigationRouter = navigationRouter
} }
} }
@@ -0,0 +1,76 @@
//
// AuthenticationCoordinatorUITests.swift
// UITests
//
// Created by Doug on 30/06/2022.
// Copyright © 2022 Element. All rights reserved.
//
import XCTest
@testable import ElementX
@MainActor
class AuthenticationCoordinatorUITests: XCTestCase {
func testLoginWithPassword() {
// Given the authentication flow.
let app = Application.launch()
app.goToScreenWithIdentifier(.authenticationFlow)
// Splash Screen: Tap get started button
app.buttons["getStartedButton"].tap()
// Login Screen: Enter valid credentials
app.textFields["usernameTextField"].tap()
app.typeText("alice\n")
app.secureTextFields["passwordTextField"].tap()
app.typeText("12345678")
// Login Screen: Tap next
app.buttons["nextButton"].tap()
// Then login should succeed.
XCTAssertFalse(app.alerts.element.exists, "No alert should be shown when logging in with valid credentials.")
}
func testLoginWithIncorrectPassword() {
// Given the authentication flow.
let app = Application.launch()
app.goToScreenWithIdentifier(.authenticationFlow)
// Splash Screen: Tap get started button
app.buttons["getStartedButton"].tap()
// Login Screen: Enter invalid credentials
app.textFields["usernameTextField"].tap()
app.typeText("alice\n")
app.typeText("87654321\n")
// Then login should fail.
XCTAssertTrue(app.alerts.element.exists, "An error alert should be shown when attempting login with invalid credentials.")
}
func testSelectingOIDCServer() {
// Given the authentication flow.
let app = Application.launch()
app.goToScreenWithIdentifier(.authenticationFlow)
// Splash Screen: Tap get started button
app.buttons["getStartedButton"].tap()
// Login Screen: Tap edit server button.
XCTAssertFalse(app.buttons["oidcButton"].exists, "The OIDC button shouldn't be shown before entering a supported homeserver.")
app.buttons["editServerButton"].tap()
// Server Selection: Clear the default and enter OIDC server.
app.textFields["addressTextField"].tap()
app.textFields["addressTextField"].buttons.element.tap()
app.typeText("company.com")
// Dismiss server screen.
app.buttons["confirmButton"].tap()
// Then the login form should be updated for OIDC.
XCTAssertTrue(app.buttons["oidcButton"].waitForExistence(timeout: 1), "The OIDC button should be shown after selecting a homeserver with OIDC.")
}
}
+23 -8
View File
@@ -27,6 +27,7 @@ class LoginScreenUITests: XCTestCase {
} }
func testMatrixDotOrg() { func testMatrixDotOrg() {
// Given the initial login screen which defaults to matrix.org.
app = Application.launch() app = Application.launch()
app.goToScreenWithIdentifier(.login) app.goToScreenWithIdentifier(.login)
@@ -37,35 +38,49 @@ class LoginScreenUITests: XCTestCase {
validateNextButtonIsDisabled(for: state) validateNextButtonIsDisabled(for: state)
validateUnsupportedServerTextIsHidden(for: state) validateUnsupportedServerTextIsHidden(for: state)
// When typing in a username and password.
app.textFields.element.tap() app.textFields.element.tap()
app.typeText("@test:server.com") app.typeText("@test:matrix.org")
app.secureTextFields.element.tap() app.secureTextFields.element.tap()
app.typeText("12345678") app.typeText("12345678")
// Then the form should be ready to submit.
validateNextButtonIsEnabled(for: "matrix.org with credentials entered") validateNextButtonIsEnabled(for: "matrix.org with credentials entered")
} }
func testOIDC() { func testOIDC() {
// Given the initial login screen.
app = Application.launch() app = Application.launch()
app.goToScreenWithIdentifier(.loginOIDC) app.goToScreenWithIdentifier(.login)
// When entering a username on a homeserver that only supports OIDC.
app.textFields.element.tap()
app.typeText("@test:company.com\n")
// Then the screen should be configured for OIDC.
let state = "an OIDC only server" let state = "an OIDC only server"
validateOIDCButtonIsShown(for: state)
validateServerDescriptionIsHidden(for: state) validateServerDescriptionIsHidden(for: state)
validateLoginFormIsHidden(for: state) validateLoginFormIsHidden(for: state)
validateOIDCButtonIsShown(for: state)
validateUnsupportedServerTextIsHidden(for: state) validateUnsupportedServerTextIsHidden(for: state)
} }
func testUnsupported() { func testUnsupported() {
// Given the initial login screen.
app = Application.launch() app = Application.launch()
app.goToScreenWithIdentifier(.loginUnsupported) app.goToScreenWithIdentifier(.login)
// When entering a username on a homeserver with an unsupported flow.
app.textFields.element.tap()
app.typeText("@test:server.net\n")
// Then the screen should not allow login to continue.
let state = "an unsupported server" let state = "an unsupported server"
validateUnsupportedServerTextIsShown(for: state)
validateServerDescriptionIsHidden(for: state) validateServerDescriptionIsHidden(for: state)
validateLoginFormIsHidden(for: state) validateLoginFormIsHidden(for: state)
validateOIDCButtonIsHidden(for: state) validateOIDCButtonIsHidden(for: state)
validateUnsupportedServerTextIsShown(for: state)
} }
/// Checks that the server description label is shown. /// Checks that the server description label is shown.
@@ -78,7 +93,7 @@ class LoginScreenUITests: XCTestCase {
/// Checks that the server description label is hidden. /// Checks that the server description label is hidden.
func validateServerDescriptionIsHidden(for state: String) { func validateServerDescriptionIsHidden(for state: String) {
let descriptionLabel = app.staticTexts["serverDescriptionText"] let descriptionLabel = app.staticTexts["serverDescriptionText"]
XCTAssertFalse(descriptionLabel.exists, "The server description should be shown for \(state).") XCTAssertFalse(descriptionLabel.exists, "The server description should be hidden for \(state).")
} }
/// Checks that the username and password text fields are shown along with the next button. /// Checks that the username and password text fields are shown along with the next button.
@@ -123,7 +138,7 @@ class LoginScreenUITests: XCTestCase {
/// Checks that the OIDC button is shown on the screen. /// Checks that the OIDC button is shown on the screen.
func validateOIDCButtonIsShown(for state: String) { func validateOIDCButtonIsShown(for state: String) {
let oidcButton = app.buttons["oidcButton"] let oidcButton = app.buttons["oidcButton"]
XCTAssertTrue(oidcButton.exists, "The OIDC button should be shown for \(state).") XCTAssertTrue(oidcButton.waitForExistence(timeout: 1), "The OIDC button should be shown for \(state).")
XCTAssertEqual(oidcButton.label, ElementL10n.loginContinue) XCTAssertEqual(oidcButton.label, ElementL10n.loginContinue)
} }
@@ -136,7 +151,7 @@ class LoginScreenUITests: XCTestCase {
/// Checks that the unsupported homeserver text is shown on the screen. /// Checks that the unsupported homeserver text is shown on the screen.
func validateUnsupportedServerTextIsShown(for state: String) { func validateUnsupportedServerTextIsShown(for state: String) {
let unsupportedText = app.staticTexts["unsupportedServerText"] let unsupportedText = app.staticTexts["unsupportedServerText"]
XCTAssertTrue(unsupportedText.exists, "The unsupported homeserver text should be shown for \(state).") XCTAssertTrue(unsupportedText.waitForExistence(timeout: 1), "The unsupported homeserver text should be shown for \(state).")
XCTAssertEqual(unsupportedText.label, ElementL10n.autodiscoverWellKnownError) XCTAssertEqual(unsupportedText.label, ElementL10n.autodiscoverWellKnownError)
} }
+2 -6
View File
@@ -20,23 +20,19 @@ import ElementX
@MainActor @MainActor
class RoomScreenUITests: XCTestCase { class RoomScreenUITests: XCTestCase {
func testPlainNoAvatar() async throws { func testPlainNoAvatar() {
let app = Application.launch() let app = Application.launch()
app.goToScreenWithIdentifier(.roomPlainNoAvatar) app.goToScreenWithIdentifier(.roomPlainNoAvatar)
try await Task.sleep(nanoseconds: 400_000_000)
XCTAssert(app.staticTexts["roomNameLabel"].exists) XCTAssert(app.staticTexts["roomNameLabel"].exists)
XCTAssert(app.staticTexts["roomAvatarPlaceholderImage"].exists) XCTAssert(app.staticTexts["roomAvatarPlaceholderImage"].exists)
XCTAssertFalse(app.images["encryptionBadgeIcon"].exists) XCTAssertFalse(app.images["encryptionBadgeIcon"].exists)
} }
func testEncryptedWithAvatar() async throws { func testEncryptedWithAvatar() {
let app = Application.launch() let app = Application.launch()
app.goToScreenWithIdentifier(.roomEncryptedWithAvatar) app.goToScreenWithIdentifier(.roomEncryptedWithAvatar)
try await Task.sleep(nanoseconds: 400_000_000)
XCTAssert(app.staticTexts["roomNameLabel"].exists) XCTAssert(app.staticTexts["roomNameLabel"].exists)
XCTAssert(app.images["roomAvatarImage"].exists) XCTAssert(app.images["roomAvatarImage"].exists)
XCTAssert(app.images["encryptionBadgeIcon"].exists) XCTAssert(app.images["encryptionBadgeIcon"].exists)
@@ -0,0 +1,100 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
import ElementX
@MainActor
class ServerSelectionUITests: XCTestCase {
let textFieldIdentifier = "addressTextField"
func testNormalState() async {
// Given the initial server selection screen as a modal.
let app = Application.launch()
app.goToScreenWithIdentifier(.serverSelection)
// Then it should be configured for matrix.org and with a cancel button
let serverTextField = app.textFields.element
XCTAssertEqual(serverTextField.value as? String, "matrix.org", "The server shown should be matrix.org with the https scheme hidden.")
let confirmButton = app.buttons["confirmButton"]
XCTAssertEqual(confirmButton.label, ElementL10n.actionConfirm, "The confirm button should say Confirm when in modal presentation.")
XCTAssertTrue(confirmButton.exists, "The confirm button should always be shown.")
XCTAssertTrue(confirmButton.isEnabled, "The confirm button should be enabled when there is an address.")
let textFieldFooter = app.staticTexts[textFieldIdentifier]
XCTAssertTrue(textFieldFooter.exists)
XCTAssertEqual(textFieldFooter.label, ElementL10n.serverSelectionServerFooter)
let dismissButton = app.buttons["dismissButton"]
XCTAssertTrue(dismissButton.exists, "The dismiss button should be shown during modal presentation.")
}
func testEmptyAddress() async {
// Given the initial server selection screen as a modal.
let app = Application.launch()
app.goToScreenWithIdentifier(.serverSelection)
// When clearing the server address text field.
app.textFields.element.tap()
app.textFields.element.buttons.element.tap()
// Then the screen should not allow the user to continue.
let serverTextField = app.textFields.element
XCTAssertEqual(serverTextField.value as? String, ElementL10n.serverSelectionServerUrl, "The text field should show placeholder text in this state.")
let confirmButton = app.buttons["confirmButton"]
XCTAssertTrue(confirmButton.exists, "The confirm button should always be shown.")
XCTAssertFalse(confirmButton.isEnabled, "The confirm button should be disabled when the address is empty.")
}
func testInvalidAddress() {
// Given the initial server selection screen as a modal.
let app = Application.launch()
app.goToScreenWithIdentifier(.serverSelection)
// When typing in an invalid homeserver
app.textFields.element.tap()
app.textFields.element.buttons.element.tap()
app.typeText("thisisbad\n") // The tests only accept an address from LoginHomeserver.mockXYZ
// Then an error should be shown and the confirmation button disabled.
let serverTextField = app.textFields.element
XCTAssertEqual(serverTextField.value as? String, "thisisbad", "The text field should show the entered server.")
let confirmButton = app.buttons["confirmButton"]
XCTAssertTrue(confirmButton.exists, "The confirm button should always be shown.")
XCTAssertFalse(confirmButton.isEnabled, "The confirm button should be disabled when there is an error.")
let textFieldFooter = app.staticTexts[textFieldIdentifier]
XCTAssertTrue(textFieldFooter.exists)
XCTAssertEqual(textFieldFooter.label, ElementL10n.loginErrorHomeserverNotFound)
}
func testNonModalPresentation() {
// Given the initial server selection screen pushed onto the stack.
let app = Application.launch()
app.goToScreenWithIdentifier(.serverSelectionNonModal)
// Then the screen should be tweaked slightly to reflect the change of navigation.
let dismissButton = app.buttons["dismissButton"]
XCTAssertFalse(dismissButton.exists, "The dismiss button should be hidden when not in modal presentation.")
let confirmButton = app.buttons["confirmButton"]
XCTAssertEqual(confirmButton.label, ElementL10n.actionNext, "The confirm button should say Next when not in modal presentation.")
}
}
+1 -4
View File
@@ -27,7 +27,7 @@ class SplashScreenUITests: XCTestCase {
XCTAssertEqual(getStartedButton.label, ElementL10n.loginSplashSubmit) XCTAssertEqual(getStartedButton.label, ElementL10n.loginSplashSubmit)
} }
func testSwipingBetweenPages() async throws { func testSwipingBetweenPages() {
let app = Application.launch() let app = Application.launch()
app.goToScreenWithIdentifier(.splash) app.goToScreenWithIdentifier(.splash)
@@ -42,7 +42,6 @@ class SplashScreenUITests: XCTestCase {
// When swiping to the next screen. // When swiping to the next screen.
page1TitleText.swipeLeft() page1TitleText.swipeLeft()
try await Task.sleep(nanoseconds: 200_000_000) // Wait for the animation.
// Then the second screen should be shown. // Then the second screen should be shown.
XCTAssertFalse(page1TitleText.isHittable, "The title from the first page of the carousel should be offscreen.") XCTAssertFalse(page1TitleText.isHittable, "The title from the first page of the carousel should be offscreen.")
@@ -50,7 +49,6 @@ class SplashScreenUITests: XCTestCase {
// When swiping back to the previous screen. // When swiping back to the previous screen.
page2TitleText.swipeRight() page2TitleText.swipeRight()
try await Task.sleep(nanoseconds: 200_000_000) // Wait for the animation.
// Then the first screen should be shown again. // Then the first screen should be shown again.
XCTAssertTrue(page1TitleText.isHittable, "The title from the first page of the carousel should be onscreen.") XCTAssertTrue(page1TitleText.isHittable, "The title from the first page of the carousel should be onscreen.")
@@ -58,7 +56,6 @@ class SplashScreenUITests: XCTestCase {
// When swiping back to the previous screen. // When swiping back to the previous screen.
page1TitleText.swipeRight() page1TitleText.swipeRight()
try await Task.sleep(nanoseconds: 200_000_000) // Wait for the animation.
// Then the screen shouldn't change and the hidden screen should be ignored. // Then the screen shouldn't change and the hidden screen should be ignored.
XCTAssertTrue(page1TitleText.isHittable, "The title from the first page of the carousel should be still be onscreen.") XCTAssertTrue(page1TitleText.isHittable, "The title from the first page of the carousel should be still be onscreen.")
@@ -136,14 +136,11 @@ class LoginViewModelTests: XCTestCase {
// Given the coordinator and view model results that contain passwords. // Given the coordinator and view model results that contain passwords.
let password = "supersecretpassword" let password = "supersecretpassword"
let viewModelAction: LoginViewModelAction = .login(username: "Alice", password: password) let viewModelAction: LoginViewModelAction = .login(username: "Alice", password: password)
let coordinatorAction: LoginCoordinatorAction = .login(username: "Alice", password: password)
// When creating a string representation of those results (e.g. for logging). // When creating a string representation of those results (e.g. for logging).
let viewModelActionString = "\(viewModelAction)" let viewModelActionString = "\(viewModelAction)"
let coordinatorActionString = "\(coordinatorAction)"
// Then the password should not be included in that string. // Then the password should not be included in that string.
XCTAssertFalse("\(viewModelActionString)".contains(password), "The password must not be included in any strings.") XCTAssertFalse("\(viewModelActionString)".contains(password), "The password must not be included in any strings.")
XCTAssertFalse("\(coordinatorActionString)".contains(password), "The password must not be included in any strings.")
} }
} }
+1
View File
@@ -0,0 +1 @@
Add AuthenticationService and missing UI tests on the flow.