From 5a1b9463bc1d301ca16afb2406ac2c7577c6e6d1 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Wed, 27 Jul 2022 10:57:16 +0100 Subject: [PATCH] #40: Use the Rust AuthenticationService. * Update SDK package to 1.0.12-alpha. * Use an app group for storage and stop stripping the http from the homeserver when configuring the service. * Rename access token to restore token. * Remove matrix.org server description inline with latest FTUE changes. --- ElementX.xcodeproj/project.pbxproj | 40 ++++++---- .../xcshareddata/swiftpm/Package.resolved | 6 +- ElementX/Sources/AppCoordinator.swift | 2 +- ElementX/Sources/BuildSettings.swift | 2 +- ElementX/Sources/Generated/InfoPlist.swift | 1 + .../AuthenticationCoordinator.swift | 64 ++++++++++++++-- .../LoginScreen/LoginCoordinator.swift | 6 +- .../LoginScreen/LoginHomeserver.swift | 34 +++------ .../LoginScreen/View/LoginScreen.swift | 3 +- .../View/LoginServerInfoSection.swift | 17 +---- .../ServerSelectionCoordinator.swift | 8 +- .../Timeline/PlaceholderAvatarImage.swift | 2 +- ...swift => AuthenticationServiceProxy.swift} | 53 ++++++++----- ... AuthenticationServiceProxyProtocol.swift} | 7 +- ...t => MockAuthenticationServiceProxy.swift} | 4 +- .../Sources/Services/Client/ClientError.swift | 2 +- .../UserSessionStore/FileManager.swift | 16 ++++ .../UserSessionStore/KeychainController.swift | 22 +++--- .../KeychainControllerProtocol.swift | 17 +++-- .../UserSessionStore/UserSessionStore.swift | 67 +++++++++-------- .../UserSessionStoreProtocol.swift | 6 +- ElementX/Sources/UITestsAppCoordinator.swift | 8 +- .../SupportingFiles/ElementX.entitlements | 4 + ElementX/SupportingFiles/Info.plist | 2 + ElementX/SupportingFiles/target.yml | 2 + UITests/Sources/LoginScreenUITests.swift | 16 ---- .../Sources/KeychainControllerTests.swift | 74 +++++++++---------- changelog.d/40.feature | 1 + project.yml | 2 +- 29 files changed, 284 insertions(+), 204 deletions(-) rename ElementX/Sources/Services/Authentication/{AuthenticationService.swift => AuthenticationServiceProxy.swift} (53%) rename ElementX/Sources/Services/Authentication/{AuthenticationServiceProtocol.swift => AuthenticationServiceProxyProtocol.swift} (72%) rename ElementX/Sources/Services/Authentication/{MockAuthenticationService.swift => MockAuthenticationServiceProxy.swift} (90%) create mode 100644 ElementX/Sources/Services/UserSessionStore/FileManager.swift create mode 100644 changelog.d/40.feature diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 46ef00fe2..8d1cb87dc 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ 132D241B09F9044711FD70A5 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; 13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */; }; 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */; }; + 152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */; }; 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */; }; 157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */; }; 15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B73D5E21F524A9BE44448D /* UserIndicatorRequest.swift */; }; @@ -73,7 +74,7 @@ 2FE4EEF780553B25A446BBFB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFA5FD06AAAC4AF544B594E /* AppDelegate.swift */; }; 30122AB3484AC6C3A7F6A717 /* ActivityIndicatorView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */; }; 3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */; }; - 32FC143630CE22A9E403370B /* MockAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */; }; + 313382FC5D38064EAAA35CB2 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D1CC633517D695FEC54208 /* FileManager.swift */; }; 33B4E59D408AE6E02323EE41 /* NoticeRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDA364DFFC3AC71C4771251 /* NoticeRoomMessage.swift */; }; 344AF4CBB6D8786214878642 /* NavigationRouterStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */; }; 34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; }; @@ -116,7 +117,6 @@ 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */; }; 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 */; }; 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; 5B2C4C17888FC095ED6880B2 /* SplashViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */; }; @@ -129,6 +129,7 @@ 617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */; }; 62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8210612D17A39369480FC183 /* MediaSource.swift */; }; 63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB10E673916D2B8D21FD197 /* TemplateModels.swift */; }; + 64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */; }; 6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */; }; 67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; 67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */; }; @@ -165,6 +166,7 @@ 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */; }; 7D1DAAA364A9A29D554BD24E /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */; }; 7DE5EB4CB2401C672257283C /* WeakKeyDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B12969CEC0051BC750DA5068 /* WeakKeyDictionary.swift */; }; + 7F08F4BC1312075E2B5EAEFA /* AuthenticationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */; }; 7F19E97E7985F518C9018B83 /* RootRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF47564C584F614B7287F3EB /* RootRouter.swift */; }; 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */; }; 7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */; }; @@ -192,7 +194,6 @@ 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; }; 9738F894DB1BD383BE05767A /* ElementSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1027BB9A852F445B7623897F /* ElementSettings.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 */; }; 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; 99ED42B8F8D6BFB1DBCF4C45 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 36B7FC232711031AA2B0D188 /* DTCoreText */; }; @@ -436,6 +437,7 @@ 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenUITests.swift; sourceTree = ""; }; 4DF56C3239EA3C16951E1E66 /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = is.lproj/Localizable.strings; sourceTree = ""; }; 4E854E7CF531DAC5CBEBDC75 /* ListTableViewAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTableViewAdapter.swift; sourceTree = ""; }; + 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxyProtocol.swift; sourceTree = ""; }; 4F49CDE349C490D617332770 /* NoticeRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItem.swift; sourceTree = ""; }; 4F5F0662483ED69791D63B16 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = et; path = et.lproj/Localizable.stringsdict; sourceTree = ""; }; 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelTests.swift; sourceTree = ""; }; @@ -455,7 +457,6 @@ 5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = ""; }; 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinatorUITests.swift; sourceTree = ""; }; 5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorTests.swift; sourceTree = ""; }; - 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProtocol.swift; sourceTree = ""; }; 5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = ""; }; 5F77E8010D41AA3F5F9A1FCA /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = ""; }; @@ -472,6 +473,7 @@ 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = ""; }; 64B23371BC8BF6164D9F6A05 /* WeakDictionaryReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionaryReference.swift; sourceTree = ""; }; 653610CB5F9776EAAAB98155 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = ""; }; + 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationServiceProxy.swift; sourceTree = ""; }; 6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = ""; }; 667DD3A9D932D7D9EB380CAA /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sk; path = sk.lproj/Localizable.stringsdict; sourceTree = ""; }; 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = ""; }; @@ -569,6 +571,7 @@ A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = ""; }; A72232816DCE2B76D48E1367 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = ""; }; A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationProtocol.swift; sourceTree = ""; }; + A8D1CC633517D695FEC54208 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; A9873374E72AA53260AE90A2 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = ""; }; AA19C32BD97F45847724E09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Untranslated.strings; sourceTree = ""; }; AAC9344689121887B74877AF /* UnitTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -636,6 +639,7 @@ CED34C87277BA3CCC6B6EC7A /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; CF3EDF23226895776553F04A /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; CF47564C584F614B7287F3EB /* RootRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouter.swift; sourceTree = ""; }; + CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = ""; }; CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementNavigationController.swift; sourceTree = ""; }; CF847B3C1873B8E81CEE7FAC /* SplashScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenViewModel.swift; sourceTree = ""; }; D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = ""; }; @@ -649,7 +653,6 @@ D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = ""; }; D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = ""; }; - DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationService.swift; sourceTree = ""; }; DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateCoordinator.swift; sourceTree = ""; }; DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = ""; }; DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenModels.swift; sourceTree = ""; }; @@ -686,7 +689,6 @@ F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBugReportService.swift; sourceTree = ""; }; F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = ""; }; F2D58333B377888012740101 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; - F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; 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 = ""; }; F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; @@ -842,6 +844,13 @@ path = Resources; sourceTree = ""; }; + 3180C73BA7B8F5F7447C99B0 /* React */ = { + isa = PBXGroup; + children = ( + ); + path = React; + sourceTree = ""; + }; 328DD5DA1281F758B72006C7 /* Views */ = { isa = PBXGroup; children = ( @@ -1218,6 +1227,7 @@ 90C85A862720155C0CF63B02 /* UserSessionStore */ = { isa = PBXGroup; children = ( + A8D1CC633517D695FEC54208 /* FileManager.swift */, 3A4427F9E0571B4E6E048A2B /* KeychainController.swift */, 0CD51F9FDC91C231906D76C8 /* KeychainControllerProtocol.swift */, 8C37FB986891D90BEAA93EAE /* UserSessionStore.swift */, @@ -1364,9 +1374,9 @@ AAFDD509929A0CCF8BCE51EB /* Authentication */ = { isa = PBXGroup; children = ( - F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */, - 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */, - DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */, + CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */, + 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */, + 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */, ); path = Authentication; sourceTree = ""; @@ -1486,6 +1496,7 @@ E74CD7681375AD2EAA34D66B /* Authentication */, 4009BE2E791C16AC6EE39A7E /* BugReport */, B53CA9BECD3F97805E1432D0 /* HomeScreen */, + 3180C73BA7B8F5F7447C99B0 /* React */, 679E9837ECA8D6776079D16E /* RoomScreen */, D958761758AA1110476DE6A3 /* SessionVerification */, 70B74A432C241E56A7ACE610 /* Settings */, @@ -1948,8 +1959,8 @@ A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */, EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */, B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */, - 9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */, - 56DACDD379A86A1F5DEFE7BE /* AuthenticationServiceProtocol.swift in Sources */, + 7F08F4BC1312075E2B5EAEFA /* AuthenticationServiceProxy.swift in Sources */, + 64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */, E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */, 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */, CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */, @@ -1981,6 +1992,7 @@ 3D325A1147F6281C57BFCDF6 /* EventBrief.swift in Sources */, 418B4AEFD03DC7A6D2C9D5C8 /* EventBriefFactory.swift in Sources */, F78C57B197DA74735FEBB42C /* EventBriefFactoryProtocol.swift in Sources */, + 313382FC5D38064EAAA35CB2 /* FileManager.swift in Sources */, A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */, 85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */, 6A367F3D7A437A79B7D9A31C /* FullscreenLoadingViewPresenter.swift in Sources */, @@ -2017,7 +2029,7 @@ A5EC21A071F58FC1229C20D0 /* MemberDetailsProviderProtocol.swift in Sources */, 24906A1E82D0046655958536 /* MessageComposer.swift in Sources */, 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */, - 32FC143630CE22A9E403370B /* MockAuthenticationService.swift in Sources */, + 152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */, 28410F3DE89C2C44E4F75C92 /* MockBugReportService.swift in Sources */, EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */, 67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */, @@ -2357,6 +2369,7 @@ 62E1B7866DF0ED442C39A83B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APP_GROUP_IDENTIFIER = group.io.element; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = ElementX/SupportingFiles/ElementX.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; @@ -2379,6 +2392,7 @@ 6897D5BC19A2EA6ABD57DE7E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APP_GROUP_IDENTIFIER = group.io.element; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = ElementX/SupportingFiles/ElementX.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; @@ -2674,7 +2688,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = "1.0.11-alpha"; + version = "1.0.12-alpha"; }; }; 9A472EE0218FE7DCF5283429 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 11ec5dbb5..721b6f2d9 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -57,10 +57,10 @@ { "identity" : "matrix-rust-components-swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/matrix-org/matrix-rust-components-swift.git", + "location" : "https://github.com/matrix-org/matrix-rust-components-swift", "state" : { - "revision" : "7d5bdf05ff97e2229cb504982162fc02c37c58e5", - "version" : "1.0.11-alpha" + "revision" : "3dbb1533d1c27dcd311a8c9751de7c020ccf2d8c", + "version" : "1.0.12-alpha" } }, { diff --git a/ElementX/Sources/AppCoordinator.swift b/ElementX/Sources/AppCoordinator.swift index 790c0559b..891724940 100644 --- a/ElementX/Sources/AppCoordinator.swift +++ b/ElementX/Sources/AppCoordinator.swift @@ -166,7 +166,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { } private func startAuthentication() { - let authenticationService = AuthenticationService(userSessionStore: userSessionStore) + let authenticationService = AuthenticationServiceProxy(userSessionStore: userSessionStore) let coordinator = AuthenticationCoordinator(authenticationService: authenticationService, navigationRouter: navigationRouter) coordinator.delegate = self diff --git a/ElementX/Sources/BuildSettings.swift b/ElementX/Sources/BuildSettings.swift index e93e8bd57..8f4dd56fd 100644 --- a/ElementX/Sources/BuildSettings.swift +++ b/ElementX/Sources/BuildSettings.swift @@ -11,7 +11,7 @@ import Foundation final class BuildSettings { // MARK: - Servers - static let defaultHomeserverURLString = "https://matrix.org" + static let defaultHomeserverAddress = "matrix.org" // MARK: - Bug report diff --git a/ElementX/Sources/Generated/InfoPlist.swift b/ElementX/Sources/Generated/InfoPlist.swift index 4c19463b7..7eddc05be 100644 --- a/ElementX/Sources/Generated/InfoPlist.swift +++ b/ElementX/Sources/Generated/InfoPlist.swift @@ -22,6 +22,7 @@ internal enum ElementInfoPlist { internal static let cfBundleVersion: String = _document["CFBundleVersion"] internal static let uiLaunchStoryboardName: String = _document["UILaunchStoryboardName"] internal static let uiSupportedInterfaceOrientations: [String] = _document["UISupportedInterfaceOrientations"] + internal static let appGroupIdentifier: String = _document["appGroupIdentifier"] } // swiftlint:enable identifier_name line_length type_body_length diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift index 0f6523b8a..d27308835 100644 --- a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift @@ -16,18 +16,21 @@ protocol AuthenticationCoordinatorDelegate: AnyObject { } class AuthenticationCoordinator: Coordinator, Presentable { - private let authenticationService: AuthenticationServiceProtocol + private let authenticationService: AuthenticationServiceProxyProtocol private let navigationRouter: NavigationRouter + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var activityIndicator: UserIndicator? - private(set) var clientProxy: ClientProxyProtocol? var childCoordinators: [Coordinator] = [] weak var delegate: AuthenticationCoordinatorDelegate? - init(authenticationService: AuthenticationServiceProtocol, + init(authenticationService: AuthenticationServiceProxyProtocol, navigationRouter: NavigationRouter) { self.authenticationService = authenticationService self.navigationRouter = navigationRouter + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: navigationRouter.toPresentable()) } func start() { @@ -47,7 +50,7 @@ class AuthenticationCoordinator: Coordinator, Presentable { guard let self = self else { return } switch action { case .login: - self.showLoginScreen() + Task { await self.startAuthentication() } } } @@ -59,19 +62,54 @@ class AuthenticationCoordinator: Coordinator, Presentable { } } + private func startAuthentication() async { + startLoading() + + switch await authenticationService.configure(for: BuildSettings.defaultHomeserverAddress) { + case .success: + stopLoading() + showLoginScreen() + case .failure: + stopLoading() + showServerSelectionScreen() + } + } + + private func showServerSelectionScreen() { + let parameters = ServerSelectionCoordinatorParameters(authenticationService: authenticationService, + hasModalPresentation: false) + let coordinator = ServerSelectionCoordinator(parameters: parameters) + + coordinator.callback = { [weak self] action in + guard let self = self else { return } + + switch action { + case .updated: + self.showLoginScreen() + case .dismiss: + MXLog.failure("[AuthenticationCoordinator] ServerSelectionScreen is requesting dismiss when part of a stack.") + } + } + + coordinator.start() + add(childCoordinator: coordinator) + + navigationRouter.push(coordinator) { [weak self] in + self?.remove(childCoordinator: coordinator) + } + } + private func showLoginScreen() { let parameters = LoginCoordinatorParameters(authenticationService: authenticationService, navigationRouter: navigationRouter) let coordinator = LoginCoordinator(parameters: parameters) - coordinator.callback = { [weak self, weak coordinator] action in - guard let self = self, let coordinator = coordinator else { return } + coordinator.callback = { [weak self] action in + guard let self = self else { return } switch action { case .signedIn(let userSession): self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession) - self.remove(childCoordinator: coordinator) - self.navigationRouter.dismissModule() case .continueWithOIDC: break } @@ -84,4 +122,14 @@ class AuthenticationCoordinator: Coordinator, Presentable { self?.remove(childCoordinator: coordinator) } } + + /// Show a blocking activity indicator. + private func startLoading() { + activityIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true)) + } + + /// Hide the currently displayed activity indicator. + private func stopLoading() { + activityIndicator = nil + } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift index 0f51a97fb..f3145a78d 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift @@ -19,7 +19,7 @@ import SwiftUI struct LoginCoordinatorParameters { /// The service used to authenticate the user. - let authenticationService: AuthenticationServiceProtocol + let authenticationService: AuthenticationServiceProxyProtocol /// The navigation router used to present the server selection screen. let navigationRouter: NavigationRouterType } @@ -46,7 +46,7 @@ final class LoginCoordinator: Coordinator, Presentable { } } - private var authenticationService: AuthenticationServiceProtocol { parameters.authenticationService } + private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService } private var navigationRouter: NavigationRouterType { parameters.navigationRouter } private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var activityIndicator: UserIndicator? @@ -163,7 +163,7 @@ final class LoginCoordinator: Coordinator, Presentable { startLoading(isInteractionBlocking: false) Task { - switch await authenticationService.startLogin(for: homeserverDomain) { + switch await authenticationService.configure(for: homeserverDomain) { case .success: updateViewModel() stopLoading() diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift index 8a06c8d3b..c332639de 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift @@ -20,20 +20,15 @@ import Foundation struct LoginHomeserver: Equatable { /// The homeserver string to be shown to the user. let address: String - /// Whether or not the homeserver is matrix.org. - let isMatrixDotOrg: Bool /// The types login supported by the homeserver. - let loginMode: LoginMode -} - -extension LoginHomeserver { - /// Temporary initialiser for use until the FFI has homeserver discovery etc. - init(address: String) { + var loginMode: LoginMode + + /// Creates a new homeserver value. + init(address: String, loginMode: LoginMode) { let address = Self.sanitized(address).components(separatedBy: "://").last ?? address self.address = address - isMatrixDotOrg = address == "matrix.org" - loginMode = .password + self.loginMode = loginMode } /// Sanitizes a user entered homeserver address with the following rules @@ -59,30 +54,23 @@ extension LoginHomeserver { extension LoginHomeserver { /// A mock homeserver that is configured just like matrix.org. static var mockMatrixDotOrg: LoginHomeserver { - LoginHomeserver(address: "matrix.org", - isMatrixDotOrg: true, - loginMode: .password) + LoginHomeserver(address: "matrix.org", loginMode: .password) } /// A mock homeserver that supports login and registration via a password but has no SSO providers. static var mockBasicServer: LoginHomeserver { - LoginHomeserver(address: "example.com", - isMatrixDotOrg: false, - loginMode: .password) + LoginHomeserver(address: "example.com", loginMode: .password) } /// A mock homeserver that supports only supports authentication via a single SSO provider. static var mockOIDC: LoginHomeserver { - LoginHomeserver(address: "company.com", - isMatrixDotOrg: false, - // swiftlint:disable:next force_unwrapping - loginMode: .oidc(URL(string: "https://auth.company.com")!)) + // swiftlint:disable:next force_unwrapping + let issuerURL = URL(string: "https://auth.company.com")! + return LoginHomeserver(address: "company.com", loginMode: .oidc(issuerURL)) } /// A mock homeserver that only with no supported login flows. static var mockUnsupported: LoginHomeserver { - LoginHomeserver(address: "server.net", - isMatrixDotOrg: false, - loginMode: .unsupported) + LoginHomeserver(address: "server.net", loginMode: .unsupported) } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift index 07a1ffca9..fd1d3a585 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift @@ -72,8 +72,7 @@ struct LoginScreen: View { /// The sever information section that includes a button to select a different server. var serverInfo: some View { - LoginServerInfoSection(address: context.viewState.homeserver.address, - showMatrixDotOrgInfo: context.viewState.homeserver.isMatrixDotOrg) { + LoginServerInfoSection(address: context.viewState.homeserver.address) { context.send(viewAction: .selectServer) } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginServerInfoSection.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginServerInfoSection.swift index 9ede010a1..649207118 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginServerInfoSection.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginServerInfoSection.swift @@ -23,8 +23,6 @@ struct LoginServerInfoSection: View { /// The address shown for the server. let address: String - /// Whether or not to show the matrix.org description. - let showMatrixDotOrgInfo: Bool /// The action performed when tapping the edit button. let editAction: () -> Void @@ -37,18 +35,9 @@ struct LoginServerInfoSection: View { .foregroundColor(.element.secondaryContent) HStack { - VStack(alignment: .leading, spacing: 2) { - Text(address) - .font(.element.body) - .foregroundColor(.element.primaryContent) - - if showMatrixDotOrgInfo { - Text(ElementL10n.authenticationServerInfoMatrixDescription) - .font(.element.caption1) - .foregroundColor(.element.tertiaryContent) - .accessibilityIdentifier("serverDescriptionText") - } - } + Text(address) + .font(.element.body) + .foregroundColor(.element.primaryContent) Spacer() diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift index 3d5e900d7..d502be05b 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift @@ -18,7 +18,7 @@ import SwiftUI struct ServerSelectionCoordinatorParameters { /// The service used to authenticate the user. - let authenticationService: AuthenticationServiceProtocol + let authenticationService: AuthenticationServiceProxyProtocol /// Whether the screen is presented modally or within a navigation stack. let hasModalPresentation: Bool } @@ -37,7 +37,7 @@ final class ServerSelectionCoordinator: Coordinator, Presentable { private let serverSelectionHostingController: UIViewController private var serverSelectionViewModel: ServerSelectionViewModelProtocol - private var authenticationService: AuthenticationServiceProtocol { parameters.authenticationService } + private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService } private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var loadingIndicator: UserIndicator? @@ -103,7 +103,7 @@ final class ServerSelectionCoordinator: Coordinator, Presentable { startLoading() Task { - switch await authenticationService.startLogin(for: homeserverAddress) { + switch await authenticationService.configure(for: homeserverAddress) { case .success: callback?(.updated) stopLoading() @@ -117,7 +117,7 @@ final class ServerSelectionCoordinator: Coordinator, Presentable { /// Processes an error to either update the flow or display it to the user. private func handleError(_ error: AuthenticationServiceError) { switch error { - case .invalidServer: + case .invalidServer, .invalidHomeserverAddress: serverSelectionViewModel.displayError(.footerMessage(ElementL10n.loginErrorHomeserverNotFound)) default: serverSelectionViewModel.displayError(.footerMessage(ElementL10n.unknownError)) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PlaceholderAvatarImage.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PlaceholderAvatarImage.swift index 6510dd6b8..27dd26168 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PlaceholderAvatarImage.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PlaceholderAvatarImage.swift @@ -26,7 +26,7 @@ struct PlaceholderAvatarImage: View { .padding(4) .foregroundColor(.white) // Make the text resizable (i.e. Make it large and then allow it to scale down) - .font(.system(size: 200)) + .font(.system(size: 200).weight(.semibold)) .minimumScaleFactor(0.001) } .aspectRatio(1, contentMode: .fill) diff --git a/ElementX/Sources/Services/Authentication/AuthenticationService.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift similarity index 53% rename from ElementX/Sources/Services/Authentication/AuthenticationService.swift rename to ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift index 618e3418c..c2e79459a 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift @@ -9,42 +9,61 @@ import Foundation import MatrixRustSDK -class AuthenticationService: AuthenticationServiceProtocol { +class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { // MARK: - Properties // MARK: Private - private(set) var homeserver = LoginHomeserver(address: BuildSettings.defaultHomeserverURLString) + private let authenticationService: AuthenticationService private let userSessionStore: UserSessionStoreProtocol + // MARK: Public + + private(set) var homeserver = LoginHomeserver(address: BuildSettings.defaultHomeserverAddress, loginMode: .unknown) + // MARK: - Setup init(userSessionStore: UserSessionStoreProtocol) { self.userSessionStore = userSessionStore + authenticationService = AuthenticationService(basePath: userSessionStore.baseDirectoryPath) } // MARK: - Public - func startLogin(for homeserverAddress: String) async -> Result { - homeserver = LoginHomeserver(address: homeserverAddress) - return .success(()) + func configure(for homeserverAddress: String) async -> Result { + let task = Task.detached { () -> LoginHomeserver in + var homeserver = LoginHomeserver(address: homeserverAddress, loginMode: .unknown) + + try self.authenticationService.configureHomeserver(serverName: homeserverAddress) + + guard let details = self.authenticationService.homeserverDetails() else { return homeserver } + + if let issuer = details.authenticationIssuer(), let issuerURL = URL(string: issuer) { + homeserver.loginMode = .oidc(issuerURL) + } else if details.supportsPasswordLogin() { + homeserver.loginMode = .password + } else { + homeserver.loginMode = .unsupported + } + + return homeserver + } + + switch await task.result { + case .success(let homeserver): + self.homeserver = homeserver + return .success(()) + case .failure(let error): + MXLog.error("Failed configuring a server: \(error)") + return .failure(.invalidHomeserverAddress) + } } func login(username: String, password: String) async -> Result { 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 = Task.detached { - let client = try builder.build() - try client.login(username: username, password: password) - return client + try self.authenticationService.login(username: username, password: password) } switch await loginTask.result { @@ -55,7 +74,7 @@ class AuthenticationService: AuthenticationServiceProtocol { Benchmark.endTrackingForIdentifier("Login", message: "Login failed") MXLog.error("Failed logging in with error: \(error)") - guard let error = error as? ClientError else { return .failure(.failedLoggingIn) } + guard let error = error as? AuthenticationError else { return .failure(.failedLoggingIn) } switch error.code { case .forbidden: diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift similarity index 72% rename from ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift rename to ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift index 8bbed5147..8bf281c77 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift @@ -1,5 +1,5 @@ // -// AuthenticationServiceProtocol.swift +// AuthenticationServiceProxyProtocol.swift // ElementX // // Created by Doug on 29/06/2022. @@ -11,16 +11,17 @@ import Foundation enum AuthenticationServiceError: Error { case invalidServer case invalidCredentials + case invalidHomeserverAddress case accountDeactivated case failedLoggingIn } @MainActor -protocol AuthenticationServiceProtocol { +protocol AuthenticationServiceProxyProtocol { var homeserver: LoginHomeserver { get } /// Sets up the service for login on the specified homeserver address. - func startLogin(for homeserverAddress: String) async -> Result + func configure(for homeserverAddress: String) async -> Result /// Performs a password login using the current homeserver. func login(username: String, password: String) async -> Result } diff --git a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift b/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift similarity index 90% rename from ElementX/Sources/Services/Authentication/MockAuthenticationService.swift rename to ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift index 1d2bab7b9..b76c4313b 100644 --- a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift @@ -8,11 +8,11 @@ import Foundation -class MockAuthenticationService: AuthenticationServiceProtocol { +class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol { let validCredentials = (username: "alice", password: "12345678") private(set) var homeserver: LoginHomeserver = .mockMatrixDotOrg - func startLogin(for homeserverAddress: String) async -> Result { + func configure(for homeserverAddress: String) async -> Result { // Map the address to the mock homeservers if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) { homeserver = .mockMatrixDotOrg diff --git a/ElementX/Sources/Services/Client/ClientError.swift b/ElementX/Sources/Services/Client/ClientError.swift index 63a30ed98..2e102e691 100644 --- a/ElementX/Sources/Services/Client/ClientError.swift +++ b/ElementX/Sources/Services/Client/ClientError.swift @@ -15,7 +15,7 @@ enum MatrixErrorCode: String, CaseIterable { case forbidden = "M_FORBIDDEN" } -extension ClientError { +extension AuthenticationError { var code: MatrixErrorCode { guard case let .Generic(message) = self else { return .unknown } diff --git a/ElementX/Sources/Services/UserSessionStore/FileManager.swift b/ElementX/Sources/Services/UserSessionStore/FileManager.swift new file mode 100644 index 000000000..dca39c313 --- /dev/null +++ b/ElementX/Sources/Services/UserSessionStore/FileManager.swift @@ -0,0 +1,16 @@ +// +// FileManager.swift +// ElementX +// +// Created by Doug on 19/07/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation + +extension FileManager { + /// The URL of the primary app group container. + var appGroupContainerURL: URL? { + containerURL(forSecurityApplicationGroupIdentifier: ElementInfoPlist.appGroupIdentifier) + } +} diff --git a/ElementX/Sources/Services/UserSessionStore/KeychainController.swift b/ElementX/Sources/Services/UserSessionStore/KeychainController.swift index 8fa65020f..1d1bd3da6 100644 --- a/ElementX/Sources/Services/UserSessionStore/KeychainController.swift +++ b/ElementX/Sources/Services/UserSessionStore/KeychainController.swift @@ -16,42 +16,42 @@ class KeychainController: KeychainControllerProtocol { keychain = Keychain(service: identifier) } - func setAccessToken(_ accessToken: String, forUsername username: String) { + func setRestoreToken(_ restoreToken: String, forUsername username: String) { do { - try keychain.set(accessToken, key: username) + try keychain.set(restoreToken, key: username) } catch { - MXLog.error("Failed storing user access token with error: \(error)") + MXLog.error("Failed storing user restore token with error: \(error)") } } - func accessTokenForUsername(_ username: String) -> String? { + func restoreTokenForUsername(_ username: String) -> String? { do { return try keychain.get(username) } catch { - MXLog.error("Failed retrieving user access token") + MXLog.error("Failed retrieving user restore token") return nil } } - func accessTokens() -> [(username: String, accessToken: String)] { + func restoreTokens() -> [KeychainCredentials] { keychain.allKeys().compactMap { username in - guard let accessToken = accessTokenForUsername(username) else { + guard let restoreToken = restoreTokenForUsername(username) else { return nil } - return (username, accessToken) + return KeychainCredentials(userID: username, restoreToken: restoreToken) } } - func removeAccessTokenForUsername(_ username: String) { + func removeRestoreTokenForUsername(_ username: String) { do { try keychain.remove(username) } catch { - MXLog.error("Failed removing access token with error: \(error)") + MXLog.error("Failed removing restore token with error: \(error)") } } - func removeAllAccessTokens() { + func removeAllRestoreTokens() { do { try keychain.removeAll() } catch { diff --git a/ElementX/Sources/Services/UserSessionStore/KeychainControllerProtocol.swift b/ElementX/Sources/Services/UserSessionStore/KeychainControllerProtocol.swift index 7d7418fd2..1778677e0 100644 --- a/ElementX/Sources/Services/UserSessionStore/KeychainControllerProtocol.swift +++ b/ElementX/Sources/Services/UserSessionStore/KeychainControllerProtocol.swift @@ -8,10 +8,15 @@ import Foundation -protocol KeychainControllerProtocol { - func setAccessToken(_ accessToken: String, forUsername username: String) - func accessTokenForUsername(_ username: String) -> String? - func accessTokens() -> [(username: String, accessToken: String)] - func removeAccessTokenForUsername(_ username: String) - func removeAllAccessTokens() +struct KeychainCredentials { + let userID: String + let restoreToken: String +} + +protocol KeychainControllerProtocol { + func setRestoreToken(_ accessToken: String, forUsername username: String) + func restoreTokenForUsername(_ username: String) -> String? + func restoreTokens() -> [KeychainCredentials] + func removeRestoreTokenForUsername(_ username: String) + func removeAllRestoreTokens() } diff --git a/ElementX/Sources/Services/UserSessionStore/UserSessionStore.swift b/ElementX/Sources/Services/UserSessionStore/UserSessionStore.swift index ed90bfc7f..2dd053192 100644 --- a/ElementX/Sources/Services/UserSessionStore/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSessionStore/UserSessionStore.swift @@ -23,7 +23,10 @@ class UserSessionStore: UserSessionStoreProtocol { private let backgroundTaskService: BackgroundTaskServiceProtocol /// Whether or not there are sessions in the store. - var hasSessions: Bool { !keychainController.accessTokens().isEmpty } + var hasSessions: Bool { !keychainController.restoreTokens().isEmpty } + + /// The base directory where all session data is stored. + var baseDirectoryPath: String { baseDirectory().path } init(bundleIdentifier: String, backgroundTaskService: BackgroundTaskServiceProtocol) { keychainController = KeychainController(identifier: bundleIdentifier) @@ -31,13 +34,13 @@ class UserSessionStore: UserSessionStoreProtocol { } func restoreUserSession() async -> Result { - let availableAccessTokens = keychainController.accessTokens() + let availableCredentials = keychainController.restoreTokens() - guard let usernameTokenTuple = availableAccessTokens.first else { + guard let credentials = availableCredentials.first else { return .failure(.missingCredentials) } - switch await restorePreviousLogin(usernameTokenTuple) { + switch await restorePreviousLogin(credentials) { case .success(let clientProxy): return .success(UserSession(clientProxy: clientProxy, mediaProvider: MediaProvider(clientProxy: clientProxy, @@ -47,8 +50,8 @@ class UserSessionStore: UserSessionStoreProtocol { MXLog.error("Failed restoring login with error: \(error)") // On any restoration failure reset the token and restart - keychainController.removeAllAccessTokens() - deleteBaseDirectory(for: usernameTokenTuple.username) + keychainController.removeAllRestoreTokens() + deleteSessionDirectory(for: credentials.userID) return .failure(error) } @@ -68,22 +71,21 @@ class UserSessionStore: UserSessionStoreProtocol { } func logout(userSession: UserSessionProtocol) { - let username = userSession.clientProxy.userIdentifier - keychainController.removeAccessTokenForUsername(username) - deleteBaseDirectory(for: username) + let userID = userSession.clientProxy.userIdentifier + keychainController.removeRestoreTokenForUsername(userID) + deleteSessionDirectory(for: userID) } - private func restorePreviousLogin(_ usernameTokenTuple: (username: String, accessToken: String)) async -> Result { + private func restorePreviousLogin(_ credentials: KeychainCredentials) async -> Result { Benchmark.startTrackingForIdentifier("Login", message: "Started restoring previous login") - let basePath = baseDirectoryPath(for: usernameTokenTuple.username) let builder = ClientBuilder() - .basePath(path: basePath) - .username(username: usernameTokenTuple.username) + .basePath(path: baseDirectoryPath) + .username(username: credentials.userID) let loginTask: Task = Task.detached { let client = try builder.build() - try client.restoreLogin(restoreToken: usernameTokenTuple.accessToken) + try client.restoreLogin(restoreToken: credentials.restoreToken) return client } @@ -101,7 +103,7 @@ class UserSessionStore: UserSessionStoreProtocol { let accessToken = try client.restoreToken() let userId = try client.userId() - keychainController.setAccessToken(accessToken, forUsername: userId) + keychainController.setRestoreToken(accessToken, forUsername: userId) } catch { MXLog.error("Failed setting up user session with error: \(error)") return .failure(.failedSettingUpSession) @@ -112,25 +114,30 @@ class UserSessionStore: UserSessionStoreProtocol { return .success(clientProxy) } - func baseDirectoryPath(for username: String) -> String { - guard var url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { - fatalError("Should always be able to retrieve the caches directory") + private func deleteSessionDirectory(for userID: String) { + // Rust sanitises the user ID replacing invalid characters with an _ + let sanitisedUserID = userID.replacingOccurrences(of: ":", with: "_") + let url = baseDirectory().appendingPathComponent(sanitisedUserID) + + do { + try FileManager.default.removeItem(at: url) + } catch { + MXLog.failure("Failed deleting the session data: \(error)") + } + } + + func baseDirectory() -> URL { + guard let appGroupContainerURL = FileManager.default.appGroupContainerURL else { + fatalError("Should always be able to retrieve the container directory") } - url = url.appendingPathComponent(username) + let url = appGroupContainerURL + .appendingPathComponent("Library", isDirectory: true) + .appendingPathComponent("Caches", isDirectory: true) + .appendingPathComponent("Sessions", isDirectory: true) try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil) - return url.path - } - - private func deleteBaseDirectory(for username: String) { - guard var url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { - fatalError("Should always be able to retrieve the caches directory") - } - - url = url.appendingPathComponent(username) - - try? FileManager.default.removeItem(at: url) + return url } } diff --git a/ElementX/Sources/Services/UserSessionStore/UserSessionStoreProtocol.swift b/ElementX/Sources/Services/UserSessionStore/UserSessionStoreProtocol.swift index 0790f7e6a..3ee73555a 100644 --- a/ElementX/Sources/Services/UserSessionStore/UserSessionStoreProtocol.swift +++ b/ElementX/Sources/Services/UserSessionStore/UserSessionStoreProtocol.swift @@ -28,6 +28,9 @@ protocol UserSessionStoreProtocol { /// Whether or not there are sessions in the store. var hasSessions: Bool { get } + /// Returns the location to store user data for a particular username. + var baseDirectoryPath: String { get } + /// Restores an existing user session. func restoreUserSession() async -> Result @@ -36,7 +39,4 @@ protocol UserSessionStoreProtocol { /// Logs out of the specified session. func logout(userSession: UserSessionProtocol) - - /// Returns the location to store user data for a particular username. - func baseDirectoryPath(for username: String) -> String } diff --git a/ElementX/Sources/UITestsAppCoordinator.swift b/ElementX/Sources/UITestsAppCoordinator.swift index 551c327c1..3e97f42e6 100644 --- a/ElementX/Sources/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITestsAppCoordinator.swift @@ -60,16 +60,16 @@ class MockScreen: Identifiable { lazy var coordinator: Coordinator & Presentable = { switch id { case .login: - return LoginCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), + return LoginCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(), navigationRouter: navigationRouter)) case .serverSelection: - return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), + return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(), hasModalPresentation: true)) case .serverSelectionNonModal: - return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), + return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(), hasModalPresentation: false)) case .authenticationFlow: - return AuthenticationCoordinator(authenticationService: MockAuthenticationService(), + return AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(), navigationRouter: navigationRouter) case .simpleRegular: return TemplateCoordinator(parameters: .init(promptType: .regular)) diff --git a/ElementX/SupportingFiles/ElementX.entitlements b/ElementX/SupportingFiles/ElementX.entitlements index 7bee5583c..4ead86aa7 100644 --- a/ElementX/SupportingFiles/ElementX.entitlements +++ b/ElementX/SupportingFiles/ElementX.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + $(APP_GROUP_IDENTIFIER) + com.apple.security.network.client keychain-access-groups diff --git a/ElementX/SupportingFiles/Info.plist b/ElementX/SupportingFiles/Info.plist index 5d181aad9..49fd47fa1 100644 --- a/ElementX/SupportingFiles/Info.plist +++ b/ElementX/SupportingFiles/Info.plist @@ -27,5 +27,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + appGroupIdentifier + $(APP_GROUP_IDENTIFIER) diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index b7dc5d3a4..1c0ac5e35 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -46,11 +46,13 @@ targets: UIInterfaceOrientationLandscapeLeft, UIInterfaceOrientationLandscapeRight ] + appGroupIdentifier: $(APP_GROUP_IDENTIFIER) settings: base: PRODUCT_NAME: ElementX PRODUCT_BUNDLE_IDENTIFIER: io.element.elementx + APP_GROUP_IDENTIFIER: group.io.element MARKETING_VERSION: 1.0.2 CURRENT_PROJECT_VERSION: 1 DEVELOPMENT_TEAM: 7J4U792NQT diff --git a/UITests/Sources/LoginScreenUITests.swift b/UITests/Sources/LoginScreenUITests.swift index b03edf133..0b15b28ad 100644 --- a/UITests/Sources/LoginScreenUITests.swift +++ b/UITests/Sources/LoginScreenUITests.swift @@ -32,7 +32,6 @@ class LoginScreenUITests: XCTestCase { app.goToScreenWithIdentifier(.login) let state = "matrix.org" - validateServerDescriptionIsVisible(for: state) validateLoginFormIsVisible(for: state) validateOIDCButtonIsHidden(for: state) validateNextButtonIsDisabled(for: state) @@ -61,7 +60,6 @@ class LoginScreenUITests: XCTestCase { // Then the screen should be configured for OIDC. let state = "an OIDC only server" validateOIDCButtonIsShown(for: state) - validateServerDescriptionIsHidden(for: state) validateLoginFormIsHidden(for: state) validateUnsupportedServerTextIsHidden(for: state) } @@ -78,24 +76,10 @@ class LoginScreenUITests: XCTestCase { // Then the screen should not allow login to continue. let state = "an unsupported server" validateUnsupportedServerTextIsShown(for: state) - validateServerDescriptionIsHidden(for: state) validateLoginFormIsHidden(for: state) validateOIDCButtonIsHidden(for: state) } - /// Checks that the server description label is shown. - func validateServerDescriptionIsVisible(for state: String) { - let descriptionLabel = app.staticTexts["serverDescriptionText"] - XCTAssertTrue(descriptionLabel.exists, "The server description should be shown for \(state).") - XCTAssertEqual(descriptionLabel.label, ElementL10n.authenticationServerInfoMatrixDescription) - } - - /// Checks that the server description label is hidden. - func validateServerDescriptionIsHidden(for state: String) { - let descriptionLabel = app.staticTexts["serverDescriptionText"] - 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. func validateLoginFormIsVisible(for state: String) { let usernameTextField = app.textFields.element diff --git a/UnitTests/Sources/KeychainControllerTests.swift b/UnitTests/Sources/KeychainControllerTests.swift index ac7d254df..5e1946193 100644 --- a/UnitTests/Sources/KeychainControllerTests.swift +++ b/UnitTests/Sources/KeychainControllerTests.swift @@ -22,68 +22,68 @@ class KeychainControllerTests: XCTestCase { override func setUp() { keychain = KeychainController(identifier: "\(ElementInfoPlist.cfBundleIdentifier).tests") - keychain.removeAllAccessTokens() + keychain.removeAllRestoreTokens() } - func testAddAccessToken() { + func testAddRestoreToken() { // Given an empty keychain. - XCTAssertTrue(keychain.accessTokens().isEmpty, "The keychain should be empty to begin with.") + XCTAssertTrue(keychain.restoreTokens().isEmpty, "The keychain should be empty to begin with.") - // When adding an access token. + // When adding an restore token. let username = "@test:example.com" - let accessToken = UUID().uuidString - keychain.setAccessToken(accessToken, forUsername: username) + let restoreToken = UUID().uuidString + keychain.setRestoreToken(restoreToken, forUsername: username) - // Then the access token should be stored in the keychain. - XCTAssertEqual(keychain.accessTokenForUsername(username), accessToken, "The retrieved access token should match the value that was stored.") + // Then the restore token should be stored in the keychain. + XCTAssertEqual(keychain.restoreTokenForUsername(username), restoreToken, "The retrieved restore token should match the value that was stored.") } - func testRemovingAccessToken() { - // Given a keychain with a stored access token. + func testRemovingRestoreToken() { + // Given a keychain with a stored restore token. let username = "@test:example.com" - let accessToken = UUID().uuidString - keychain.setAccessToken(accessToken, forUsername: username) - XCTAssertEqual(keychain.accessTokens().count, 1, "The keychain should have 1 access token.") - XCTAssertEqual(keychain.accessTokenForUsername(username), accessToken, "The initial access token should match the value that was stored.") + let restoreToken = UUID().uuidString + keychain.setRestoreToken(restoreToken, forUsername: username) + XCTAssertEqual(keychain.restoreTokens().count, 1, "The keychain should have 1 restore token.") + XCTAssertEqual(keychain.restoreTokenForUsername(username), restoreToken, "The initial restore token should match the value that was stored.") - // When deleting the access token. - keychain.removeAccessTokenForUsername(username) + // When deleting the restore token. + keychain.removeRestoreTokenForUsername(username) // Then the keychain should be empty. - XCTAssertTrue(keychain.accessTokens().isEmpty, "The keychain should be empty after deleting the token.") - XCTAssertNil(keychain.accessTokenForUsername(username), "There access token should not be returned after removal.") + XCTAssertTrue(keychain.restoreTokens().isEmpty, "The keychain should be empty after deleting the token.") + XCTAssertNil(keychain.restoreTokenForUsername(username), "There restore token should not be returned after removal.") } - func testRemovingAllAccessTokens() { - // Given a keychain with 5 stored access tokens. + func testRemovingAllRestoreTokens() { + // Given a keychain with 5 stored restore tokens. for index in 0..<5 { - keychain.setAccessToken(UUID().uuidString, forUsername: "@test\(index):example.com") + keychain.setRestoreToken(UUID().uuidString, forUsername: "@test\(index):example.com") } - XCTAssertEqual(keychain.accessTokens().count, 5, "The keychain should have 5 access tokens.") + XCTAssertEqual(keychain.restoreTokens().count, 5, "The keychain should have 5 restore tokens.") - // When deleting all of the access tokens. - keychain.removeAllAccessTokens() + // When deleting all of the restore tokens. + keychain.removeAllRestoreTokens() // Then the keychain should be empty. - XCTAssertTrue(keychain.accessTokens().isEmpty, "The keychain should be empty after deleting the token.") + XCTAssertTrue(keychain.restoreTokens().isEmpty, "The keychain should be empty after deleting the token.") } - func testRemovingSingleAccessTokens() { - // Given a keychain with 5 stored access tokens. + func testRemovingSingleRestoreTokens() { + // Given a keychain with 5 stored restore tokens. for index in 0..<5 { - keychain.setAccessToken(UUID().uuidString, forUsername: "@test\(index):example.com") + keychain.setRestoreToken(UUID().uuidString, forUsername: "@test\(index):example.com") } - XCTAssertEqual(keychain.accessTokens().count, 5, "The keychain should have 5 access tokens.") + XCTAssertEqual(keychain.restoreTokens().count, 5, "The keychain should have 5 restore tokens.") - // When deleting one of the access tokens. - keychain.removeAccessTokenForUsername("@test2:example.com") + // When deleting one of the restore tokens. + keychain.removeRestoreTokenForUsername("@test2:example.com") // Then the other 4 items should remain untouched. - XCTAssertEqual(keychain.accessTokens().count, 4, "The keychain have 4 remaining access tokens.") - XCTAssertNotNil(keychain.accessTokenForUsername("@test0:example.com"), "The access token should not have been deleted.") - XCTAssertNotNil(keychain.accessTokenForUsername("@test1:example.com"), "The access token should not have been deleted.") - XCTAssertNil(keychain.accessTokenForUsername("@test2:example.com"), "The access token should have been deleted.") - XCTAssertNotNil(keychain.accessTokenForUsername("@test3:example.com"), "The access token should not have been deleted.") - XCTAssertNotNil(keychain.accessTokenForUsername("@test4:example.com"), "The access token should not have been deleted.") + XCTAssertEqual(keychain.restoreTokens().count, 4, "The keychain have 4 remaining restore tokens.") + XCTAssertNotNil(keychain.restoreTokenForUsername("@test0:example.com"), "The restore token should not have been deleted.") + XCTAssertNotNil(keychain.restoreTokenForUsername("@test1:example.com"), "The restore token should not have been deleted.") + XCTAssertNil(keychain.restoreTokenForUsername("@test2:example.com"), "The restore token should have been deleted.") + XCTAssertNotNil(keychain.restoreTokenForUsername("@test3:example.com"), "The restore token should not have been deleted.") + XCTAssertNotNil(keychain.restoreTokenForUsername("@test4:example.com"), "The restore token should not have been deleted.") } } diff --git a/changelog.d/40.feature b/changelog.d/40.feature new file mode 100644 index 000000000..ec0252714 --- /dev/null +++ b/changelog.d/40.feature @@ -0,0 +1 @@ +Perform password login using the Rust authentication service. diff --git a/project.yml b/project.yml index 7cd8c583f..81abf03a7 100644 --- a/project.yml +++ b/project.yml @@ -31,7 +31,7 @@ include: packages: MatrixRustSDK: url: https://github.com/matrix-org/matrix-rust-components-swift - exactVersion: 1.0.11-alpha + exactVersion: 1.0.12-alpha # path: ../matrix-rust-components-swift DesignKit: path: ./