diff --git a/CHANGES.rst b/CHANGES.rst index befac451d..7f07af131 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,11 +5,10 @@ Changes to be released in next version * 🙌 Improvements - * Social login: Handle new identity provider brand field in order to customize buttons (#3980). - * Widgets: Support $matrix_room_id and $matrix_widget_id parameters (#3987). + * 🐛 Bugfix - * Push: Fix PushKit crashes due to undecryptable call invites (#3986). + * ⚠️ API Changes * @@ -23,6 +22,41 @@ Changes to be released in next version Others * +Changes in 1.1.7 (2021-02-03) +================================================= + +✨ Features + * + +🙌 Improvements + * Social login: Handle new identity provider brand field in order to customize buttons (#3980). + * Widgets: Support $matrix_room_id and $matrix_widget_id parameters (#3987). + * matrix.to: Support room preview when the permalink has parameters (like "via="). + * Avoid megolm share requests if the device is not verified (#3969) + * Handle User-Interactive Authentication fallback (#3995). + +🐛 Bugfix + * Push: Fix PushKit crashes due to undecryptable call invites (#3986). + * matrix.to: Cannot open links with query parameters (#3990). + * matrix.to: Cannot open/preview a new room given by alias (#3991). + * matrix.to: The app does not open a permalink from matrix.to (#3993). + * Logs: Add a size limitation so that we can upload them in bug reports (#3903). + +⚠️ API Changes + * + +🗣 Translations + * + +🧱 Build + * + +Others + * + +Improvements: + * Upgrade MatrixKit version ([v0.13.9](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.13.9)). + Changes in 1.1.6 (2021-01-27) ================================================= diff --git a/Podfile b/Podfile index 38349e222..c4d7b8e80 100644 --- a/Podfile +++ b/Podfile @@ -11,7 +11,7 @@ use_frameworks! # - `{ {kit spec hash} => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for each repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixKitVersion = '= 0.13.8' +$matrixKitVersion = '= 0.13.9' # $matrixKitVersion = :local # $matrixKitVersion = {'develop' => 'develop'} diff --git a/Podfile.lock b/Podfile.lock index 7f785686f..ce24dd132 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -60,35 +60,35 @@ PODS: - MatomoTracker (7.2.2): - MatomoTracker/Core (= 7.2.2) - MatomoTracker/Core (7.2.2) - - MatrixKit (0.13.8): + - MatrixKit (0.13.9): - Down (~> 0.9.3) - DTCoreText (~> 1.6.23) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixKit/Core (= 0.13.8) - - MatrixSDK (= 0.17.10) - - MatrixKit/AppExtension (0.13.8): + - MatrixKit/Core (= 0.13.9) + - MatrixSDK (= 0.17.11) + - MatrixKit/AppExtension (0.13.9): - Down (~> 0.9.3) - DTCoreText (~> 1.6.23) - DTCoreText/Extension - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.17.10) - - MatrixKit/Core (0.13.8): + - MatrixSDK (= 0.17.11) + - MatrixKit/Core (0.13.9): - Down (~> 0.9.3) - DTCoreText (~> 1.6.23) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.17.10) - - MatrixSDK (0.17.10): - - MatrixSDK/Core (= 0.17.10) - - MatrixSDK/Core (0.17.10): + - MatrixSDK (= 0.17.11) + - MatrixSDK (0.17.11): + - MatrixSDK/Core (= 0.17.11) + - MatrixSDK/Core (0.17.11): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - OLMKit (~> 3.1.0) - Realm (= 10.1.4) - - MatrixSDK/JingleCallStack (0.17.10): + - MatrixSDK/JingleCallStack (0.17.11): - JitsiMeetSDK (= 2.11.0) - MatrixSDK/Core - OLMKit (3.1.0): @@ -127,8 +127,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.1) - KTCenterFlowLayout (~> 1.3.1) - MatomoTracker (~> 7.2.2) - - MatrixKit (= 0.13.8) - - MatrixKit/AppExtension (= 0.13.8) + - MatrixKit (= 0.13.9) + - MatrixKit/AppExtension (= 0.13.9) - MatrixSDK - MatrixSDK/JingleCallStack - OLMKit @@ -199,8 +199,8 @@ SPEC CHECKSUMS: LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b MatomoTracker: a59ec4da0f580be57bdc6baa708a71a86532a832 - MatrixKit: 9769b1bc8fdee5f28e2a474762d964e5a3c43079 - MatrixSDK: cd11675c38dcb684843e922026ecb620251e148e + MatrixKit: 655625979013f387d351b828103465b4c6899333 + MatrixSDK: 55e0b78c2a5cdb48bdcff9f21c159f57fd803dc3 OLMKit: 4ee0159d63feeb86d836fdcfefe418e163511639 ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d Realm: 80f4fb2971ccb9adc27a47d0955ae8e533a7030b @@ -212,6 +212,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 376eeb13ecb078f5d76166cd2982477f181e815f +PODFILE CHECKSUM: 26a42f7550dcb03d4048e4bb54cd711adda25e07 COCOAPODS: 1.10.0 diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index bc4edbad4..9a537c4a0 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -280,6 +280,13 @@ B158253B2475350A00604D79 /* EventFormatter+DTCoreTextFix.m in Sources */ = {isa = PBXBuildFile; fileRef = B158253A2475350A00604D79 /* EventFormatter+DTCoreTextFix.m */; }; B158E72125485B9E000A7F75 /* Base32Coder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B158E72025485B9E000A7F75 /* Base32Coder.swift */; }; B158E72325485EAB000A7F75 /* JitsiJWTTokenBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B158E72225485EAB000A7F75 /* JitsiJWTTokenBuilder.swift */; }; + B1622D1825BF1A0000C73AE2 /* AuthenticationSessionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1622D1725BF1A0000C73AE2 /* AuthenticationSessionService.swift */; }; + B1622D1D25BF2B6800C73AE2 /* ReauthenticationCoordinatorBridgePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1622D1A25BF2B6800C73AE2 /* ReauthenticationCoordinatorBridgePresenter.swift */; }; + B1622D1E25BF2B6800C73AE2 /* ReauthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1622D1B25BF2B6800C73AE2 /* ReauthenticationCoordinator.swift */; }; + B1622D1F25BF2B6800C73AE2 /* ReauthenticationCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1622D1C25BF2B6800C73AE2 /* ReauthenticationCoordinatorType.swift */; }; + B1622D2125BF2B7B00C73AE2 /* ReauthenticationCoordinatorParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1622D2025BF2B7B00C73AE2 /* ReauthenticationCoordinatorParameters.swift */; }; + B1622D2325BF2E6500C73AE2 /* AuthenticationSessionParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1622D2225BF2E6500C73AE2 /* AuthenticationSessionParameters.swift */; }; + B1622D2525BF784300C73AE2 /* AuthenticationParametersBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1622D2425BF784300C73AE2 /* AuthenticationParametersBuilder.swift */; }; B1664BC520F4E67600808783 /* FallbackViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1664BAD20F4E67500808783 /* FallbackViewController.xib */; }; B1664BC620F4E67600808783 /* FallbackViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B1664BAE20F4E67500808783 /* FallbackViewController.m */; }; B1664BC720F4E67600808783 /* SharePresentingViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B1664BB220F4E67500808783 /* SharePresentingViewController.m */; }; @@ -318,6 +325,10 @@ B169331520F3CAFC00746532 /* PublicRoomTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B169330F20F3CAFC00746532 /* PublicRoomTableViewCell.xib */; }; B169331620F3CAFC00746532 /* PublicRoomsDirectoryDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = B169331220F3CAFC00746532 /* PublicRoomsDirectoryDataSource.m */; }; B169331720F3CBE000746532 /* RecentCellData.m in Sources */ = {isa = PBXBuildFile; fileRef = B16932F920F3C51900746532 /* RecentCellData.m */; }; + B169746A25C21BDC00E6B3FB /* CrossSigningSetupCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B169746725C21BDC00E6B3FB /* CrossSigningSetupCoordinatorType.swift */; }; + B169746B25C21BDC00E6B3FB /* CrossSigningSetupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B169746825C21BDC00E6B3FB /* CrossSigningSetupCoordinator.swift */; }; + B169746C25C21BDC00E6B3FB /* CrossSigningSetupCoordinatorBridgePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B169746925C21BDC00E6B3FB /* CrossSigningSetupCoordinatorBridgePresenter.swift */; }; + B169746F25C21C1900E6B3FB /* CrossSigningSetupCoordinatorParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = B169746E25C21C1900E6B3FB /* CrossSigningSetupCoordinatorParameters.swift */; }; B16C028D25A71CA3008CA7B1 /* CustomSchemeURLConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B16C028C25A71CA3008CA7B1 /* CustomSchemeURLConstants.swift */; }; B16C029025ABC428008CA7B1 /* CustomRoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B16C028F25ABC427008CA7B1 /* CustomRoundedButton.swift */; }; B16C029225ABCD31008CA7B1 /* InviteFriendsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B16C029125ABCD31008CA7B1 /* InviteFriendsPresenter.swift */; }; @@ -332,6 +343,8 @@ B16DC23E258A8025004DAB1A /* ThemeIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B16DC23D258A8025004DAB1A /* ThemeIdentifier.swift */; }; B17982FF2119FED2001FD722 /* GDPRConsentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17982FE2119FED2001FD722 /* GDPRConsentViewController.swift */; }; B1798302211B13B3001FD722 /* OnBoardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1798301211B13B3001FD722 /* OnBoardingManager.swift */; }; + B17DA77925C81A2B003FA4C1 /* AuthenticationSessionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17DA77825C81A2B003FA4C1 /* AuthenticationSessionServiceTests.swift */; }; + B17DA77B25C8B085003FA4C1 /* ReauthFallBackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17DA77A25C8B085003FA4C1 /* ReauthFallBackViewController.swift */; }; B183226623F55D6B0035B2E8 /* CameraAccessManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B183226523F55D6B0035B2E8 /* CameraAccessManager.swift */; }; B183226823F561380035B2E8 /* CameraAccessAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B183226723F561380035B2E8 /* CameraAccessAlertPresenter.swift */; }; B183226C23F59F810035B2E8 /* CloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B183226B23F59F810035B2E8 /* CloseButton.swift */; }; @@ -1364,6 +1377,13 @@ B158253A2475350A00604D79 /* EventFormatter+DTCoreTextFix.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "EventFormatter+DTCoreTextFix.m"; sourceTree = ""; }; B158E72025485B9E000A7F75 /* Base32Coder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base32Coder.swift; sourceTree = ""; }; B158E72225485EAB000A7F75 /* JitsiJWTTokenBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiJWTTokenBuilder.swift; sourceTree = ""; }; + B1622D1725BF1A0000C73AE2 /* AuthenticationSessionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationSessionService.swift; sourceTree = ""; }; + B1622D1A25BF2B6800C73AE2 /* ReauthenticationCoordinatorBridgePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReauthenticationCoordinatorBridgePresenter.swift; sourceTree = ""; }; + B1622D1B25BF2B6800C73AE2 /* ReauthenticationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReauthenticationCoordinator.swift; sourceTree = ""; }; + B1622D1C25BF2B6800C73AE2 /* ReauthenticationCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReauthenticationCoordinatorType.swift; sourceTree = ""; }; + B1622D2025BF2B7B00C73AE2 /* ReauthenticationCoordinatorParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReauthenticationCoordinatorParameters.swift; sourceTree = ""; }; + B1622D2225BF2E6500C73AE2 /* AuthenticationSessionParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationSessionParameters.swift; sourceTree = ""; }; + B1622D2425BF784300C73AE2 /* AuthenticationParametersBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationParametersBuilder.swift; sourceTree = ""; }; B1664BAD20F4E67500808783 /* FallbackViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = FallbackViewController.xib; sourceTree = ""; }; B1664BAE20F4E67500808783 /* FallbackViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FallbackViewController.m; sourceTree = ""; }; B1664BAF20F4E67500808783 /* FallbackViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FallbackViewController.h; sourceTree = ""; }; @@ -1450,6 +1470,10 @@ B169331020F3CAFC00746532 /* PublicRoomTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PublicRoomTableViewCell.h; sourceTree = ""; }; B169331220F3CAFC00746532 /* PublicRoomsDirectoryDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PublicRoomsDirectoryDataSource.m; sourceTree = ""; }; B169331320F3CAFC00746532 /* PublicRoomsDirectoryDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PublicRoomsDirectoryDataSource.h; sourceTree = ""; }; + B169746725C21BDC00E6B3FB /* CrossSigningSetupCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrossSigningSetupCoordinatorType.swift; sourceTree = ""; }; + B169746825C21BDC00E6B3FB /* CrossSigningSetupCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrossSigningSetupCoordinator.swift; sourceTree = ""; }; + B169746925C21BDC00E6B3FB /* CrossSigningSetupCoordinatorBridgePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrossSigningSetupCoordinatorBridgePresenter.swift; sourceTree = ""; }; + B169746E25C21C1900E6B3FB /* CrossSigningSetupCoordinatorParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossSigningSetupCoordinatorParameters.swift; sourceTree = ""; }; B16C028C25A71CA3008CA7B1 /* CustomSchemeURLConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSchemeURLConstants.swift; sourceTree = ""; }; B16C028F25ABC427008CA7B1 /* CustomRoundedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomRoundedButton.swift; sourceTree = ""; }; B16C029125ABCD31008CA7B1 /* InviteFriendsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteFriendsPresenter.swift; sourceTree = ""; }; @@ -1464,6 +1488,8 @@ B16DC23D258A8025004DAB1A /* ThemeIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeIdentifier.swift; sourceTree = ""; }; B17982FE2119FED2001FD722 /* GDPRConsentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GDPRConsentViewController.swift; sourceTree = ""; }; B1798301211B13B3001FD722 /* OnBoardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingManager.swift; sourceTree = ""; }; + B17DA77825C81A2B003FA4C1 /* AuthenticationSessionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationSessionServiceTests.swift; sourceTree = ""; }; + B17DA77A25C8B085003FA4C1 /* ReauthFallBackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReauthFallBackViewController.swift; sourceTree = ""; }; B183226523F55D6B0035B2E8 /* CameraAccessManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraAccessManager.swift; sourceTree = ""; }; B183226723F561380035B2E8 /* CameraAccessAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraAccessAlertPresenter.swift; sourceTree = ""; }; B183226B23F59F810035B2E8 /* CloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseButton.swift; sourceTree = ""; }; @@ -3056,6 +3082,18 @@ path = ThreePidDetails; sourceTree = ""; }; + B1622D1925BF2B6800C73AE2 /* Reauthentication */ = { + isa = PBXGroup; + children = ( + B1622D1C25BF2B6800C73AE2 /* ReauthenticationCoordinatorType.swift */, + B1622D1B25BF2B6800C73AE2 /* ReauthenticationCoordinator.swift */, + B1622D2025BF2B7B00C73AE2 /* ReauthenticationCoordinatorParameters.swift */, + B1622D1A25BF2B6800C73AE2 /* ReauthenticationCoordinatorBridgePresenter.swift */, + B17DA77A25C8B085003FA4C1 /* ReauthFallBackViewController.swift */, + ); + path = Reauthentication; + sourceTree = ""; + }; B1664BAB20F4E67500808783 /* Modules */ = { isa = PBXGroup; children = ( @@ -3300,6 +3338,17 @@ path = DataSources; sourceTree = ""; }; + B169746D25C21BE100E6B3FB /* Setup */ = { + isa = PBXGroup; + children = ( + B169746E25C21C1900E6B3FB /* CrossSigningSetupCoordinatorParameters.swift */, + B169746825C21BDC00E6B3FB /* CrossSigningSetupCoordinator.swift */, + B169746925C21BDC00E6B3FB /* CrossSigningSetupCoordinatorBridgePresenter.swift */, + B169746725C21BDC00E6B3FB /* CrossSigningSetupCoordinatorType.swift */, + ); + path = Setup; + sourceTree = ""; + }; B16DC2192587AED2004DAB1A /* SSO */ = { isa = PBXGroup; children = ( @@ -3547,6 +3596,7 @@ B1560DA024B65A9500490F50 /* LaunchLoading */, B1B556CA20EE6C4C00210D55 /* TabBar */, B1B556F920EE6C4C00210D55 /* Authentication */, + B1622D1925BF2B6800C73AE2 /* Reauthentication */, B17982FD2119FEA7001FD722 /* GDPR */, B1A6805224B7C60900E312CC /* MajorUpdate */, B1B5568420EE6C4C00210D55 /* Home */, @@ -5044,6 +5094,7 @@ isa = PBXGroup; children = ( EC3B066424AC6ADD000DF9BF /* CrossSigningService.swift */, + B169746D25C21BE100E6B3FB /* Setup */, EC3B066524AC6ADD000DF9BF /* Banners */, ); path = CrossSigning; @@ -5254,6 +5305,9 @@ isa = PBXGroup; children = ( EC711BB524A63C11008F830C /* AuthenticatedSessionViewControllerFactory.swift */, + B1622D1725BF1A0000C73AE2 /* AuthenticationSessionService.swift */, + B1622D2225BF2E6500C73AE2 /* AuthenticationSessionParameters.swift */, + B1622D2425BF784300C73AE2 /* AuthenticationParametersBuilder.swift */, ); path = AuthenticatedSession; sourceTree = ""; @@ -5516,6 +5570,7 @@ F083BB041E7005FD00A9B29C /* RiotTests.m */, B152C73022DF561E0041315A /* EmojiServiceTests.swift */, B1562CE42541D113002F8436 /* JitsiWellKnownTests.swift */, + B17DA77825C81A2B003FA4C1 /* AuthenticationSessionServiceTests.swift */, F083BB071E70067700A9B29C /* Supporting Files */, ); path = RiotTests; @@ -6423,6 +6478,7 @@ B1284E3E2535FEA6003529D7 /* SecureBackupSetupIntroViewModelType.swift in Sources */, B1FDF56021F5FE5500BA3834 /* KeyBackupSetupPassphraseViewAction.swift in Sources */, B1B5573120EE6C4D00210D55 /* BugReportViewController.m in Sources */, + B169746A25C21BDC00E6B3FB /* CrossSigningSetupCoordinatorType.swift in Sources */, ECFBD5CB250A7AAF00DD5F5A /* DirectoryRoomTableViewCellVM.swift in Sources */, 324A2051225FC571004FE8B0 /* DeviceVerificationIncomingViewState.swift in Sources */, B1B4E9BF24D4703E004D5C33 /* BaseBubbleCell.swift in Sources */, @@ -6530,6 +6586,7 @@ B169330B20F3CA3A00746532 /* Contact.m in Sources */, B1A5B33E227ADF2A004CBA85 /* UIImage.swift in Sources */, B113DD3D255E876A0022942A /* JitsiWellKnown.swift in Sources */, + B1622D1E25BF2B6800C73AE2 /* ReauthenticationCoordinator.swift in Sources */, EC51E7AC2514D2E100AAE7DB /* RoomInfoListCoordinatorType.swift in Sources */, B1D4752A21EE52B10067973F /* KeyBackupSetupIntroViewController.swift in Sources */, B10A3E9424FE8254007C380F /* AppCoordinatorType.swift in Sources */, @@ -6567,9 +6624,11 @@ B1A6C111238BD236002882FD /* SlidingModalContainerView.swift in Sources */, B1DCC62D22E61EAF00625807 /* EmojiPickerViewCell.swift in Sources */, 3232ABA8225730E100AD6A5C /* DeviceVerificationStartViewState.swift in Sources */, + B1622D1F25BF2B6800C73AE2 /* ReauthenticationCoordinatorType.swift in Sources */, ECF57A5525091B4D004BBF9D /* RoomCreationParameters.swift in Sources */, B1B558D420EF768F00210D55 /* RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.m in Sources */, B169331420F3CAFC00746532 /* PublicRoomTableViewCell.m in Sources */, + B1622D1D25BF2B6800C73AE2 /* ReauthenticationCoordinatorBridgePresenter.swift in Sources */, 32BF995721FB07A400698084 /* SettingsKeyBackupTableViewSection.swift in Sources */, 3291DC8D23E0BFF10009732F /* SecurityViewController.m in Sources */, B18DEDD4243377C10075FEF7 /* KeyVerificationSelfVerifyWaitViewModelType.swift in Sources */, @@ -6584,6 +6643,7 @@ B1B5571A20EE6C4D00210D55 /* SettingsViewController.m in Sources */, B1CE9EFD22148703000FAE6A /* SignOutAlertPresenter.swift in Sources */, 32F6B9692270623100BBA352 /* KeyVerificationDataLoadingCoordinator.swift in Sources */, + B1622D1825BF1A0000C73AE2 /* AuthenticationSessionService.swift in Sources */, B125FE1D231D5DE400B72806 /* SettingsDiscoveryViewModel.swift in Sources */, 32863A5A2384070300D07C4A /* RiotSharedSettings.swift in Sources */, B1B5594720EF7BD000210D55 /* RoomCollectionViewCell.m in Sources */, @@ -6706,6 +6766,7 @@ B1C45A8B232A8C2600165425 /* SettingsIdentityServerViewModel.swift in Sources */, EC711B8224A63B37008F830C /* SecretsSetupRecoveryPassphraseViewModelType.swift in Sources */, B12D7A0123E2462200FACEDC /* UserVerificationStartViewModel.swift in Sources */, + B1622D2525BF784300C73AE2 /* AuthenticationParametersBuilder.swift in Sources */, B12C56EF2396CB5E00FAC6DE /* RoomMessageURLParser.swift in Sources */, EC711B9924A63B37008F830C /* SecretsRecoveryCoordinator.swift in Sources */, B1BEE73623DF44A60003A4CB /* UserVerificationSessionsStatusCoordinatorType.swift in Sources */, @@ -6737,6 +6798,7 @@ ECF57A5E2509265A004BBF9D /* InsettedTextField.swift in Sources */, B1B5593C20EF7BAC00210D55 /* TableViewCellWithCheckBoxes.m in Sources */, 32891D6B2264CBA300C82226 /* SimpleScreenTemplateViewController.swift in Sources */, + B169746C25C21BDC00E6B3FB /* CrossSigningSetupCoordinatorBridgePresenter.swift in Sources */, EC711B7924A63B37008F830C /* SecretsSetupRecoveryKeyViewController.swift in Sources */, 32FD755024D074C700BA7B37 /* CommonConfiguration.swift in Sources */, B1CA3A2721EF6914000D1D89 /* UIViewController.swift in Sources */, @@ -6802,6 +6864,7 @@ EC711BAF24A63B58008F830C /* SecureBackupSetupCoordinatorType.swift in Sources */, EC9E9B082575AF730007C0A0 /* CallVCDismissOperation.swift in Sources */, ECFBD5D4250A7AAF00DD5F5A /* ShowDirectoryCoordinatorType.swift in Sources */, + B1622D2325BF2E6500C73AE2 /* AuthenticationSessionParameters.swift in Sources */, EC1CA89C24C9C9A200DE9EBF /* SetupBiometricsViewModel.swift in Sources */, B157FAA323264AE900EBFBD4 /* SettingsDiscoveryThreePidDetailsViewState.swift in Sources */, B1098BF921ECFE65000DDA48 /* KeyBackupSetupCoordinator.swift in Sources */, @@ -6841,6 +6904,7 @@ B14084CA23BF89310010F692 /* KeyVerificationRequestStatusWithPaginationTitleBubbleCell.swift in Sources */, B1DCC61D22E5E17100625807 /* EmojiPickerViewModelType.swift in Sources */, B1B5574120EE6C4D00210D55 /* RecentsViewController.m in Sources */, + B1622D2125BF2B7B00C73AE2 /* ReauthenticationCoordinatorParameters.swift in Sources */, B1D250D82118AA0A000F4E93 /* RoomPredecessorBubbleCell.m in Sources */, B12676832523E4D100BE6B98 /* SecretsResetCoordinator.swift in Sources */, B1B5577120EE702800210D55 /* StickerPickerViewController.m in Sources */, @@ -6913,6 +6977,7 @@ B1B5596620EF9E9B00210D55 /* RoomTableViewCell.m in Sources */, ECFBD5EB250F97FC00DD5F5A /* RoomCreationWithPaginationCollapsedBubbleCell.m in Sources */, EC711B7D24A63B37008F830C /* SecretsSetupRecoveryPassphraseViewModel.swift in Sources */, + B169746B25C21BDC00E6B3FB /* CrossSigningSetupCoordinator.swift in Sources */, EC711B8024A63B37008F830C /* SecretsSetupRecoveryPassphraseViewState.swift in Sources */, B1C45A89232A8C2600165425 /* SettingsIdentityServerViewController.swift in Sources */, EC85D7152477DCD7002C44C9 /* KeyVerificationScanConfirmationViewController.swift in Sources */, @@ -7063,6 +7128,7 @@ F083BDF01E7009ED00A9B29C /* UIViewController+RiotSearch.m in Sources */, 32DB557522FDADE50016329E /* ServiceTermsModalCoordinatorType.swift in Sources */, F083BDF91E7009ED00A9B29C /* RoomEmailInvitation.m in Sources */, + B17DA77B25C8B085003FA4C1 /* ReauthFallBackViewController.swift in Sources */, EC85D7272477DCF2002C44C9 /* KeyVerificationManuallyVerifyViewController.swift in Sources */, ECFBD5CF250A7AAF00DD5F5A /* DirectoryNetworkTableHeaderFooterView.swift in Sources */, B1D211E422C18E3800D939BD /* ReactionsMenuViewModelType.swift in Sources */, @@ -7124,6 +7190,7 @@ ECFBD5ED250F97FC00DD5F5A /* RoomCreationCollapsedBubbleCell.m in Sources */, B1DB4F06223015080065DBFA /* Character.swift in Sources */, EC1CA87724C82D0E00DE9EBF /* MemoryStore.swift in Sources */, + B169746F25C21C1900E6B3FB /* CrossSigningSetupCoordinatorParameters.swift in Sources */, B1B9DEEE22EB34EF0065E677 /* ReactionHistoryViewAction.swift in Sources */, EC711BB624A63C11008F830C /* AuthenticatedSessionViewControllerFactory.swift in Sources */, B1C543A4239E98E400DCA1FA /* KeyVerificationCellInnerContentView.swift in Sources */, @@ -7152,6 +7219,7 @@ 32FD754D24D06EBF00BA7B37 /* RiotSettings.swift in Sources */, B1562CE52541D113002F8436 /* JitsiWellKnownTests.swift in Sources */, 32FD757124D2BEF700BA7B37 /* InfoPlist.swift in Sources */, + B17DA77925C81A2B003FA4C1 /* AuthenticationSessionServiceTests.swift in Sources */, 32FD756524D2AD5100BA7B37 /* BuildSettings.swift in Sources */, 32FD755524D0754500BA7B37 /* AppConfiguration.swift in Sources */, 32FD755124D074C700BA7B37 /* CommonConfiguration.swift in Sources */, @@ -7553,7 +7621,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1.1.7; + CURRENT_PROJECT_VERSION = 1.1.8; DEFINES_MODULE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -7573,7 +7641,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 1.1.7; + MARKETING_VERSION = 1.1.8; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -7612,7 +7680,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 1.1.7; + CURRENT_PROJECT_VERSION = 1.1.8; DEFINES_MODULE = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -7625,7 +7693,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 1.1.7; + MARKETING_VERSION = 1.1.8; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index c7dfdf2cf..980815ffd 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -150,8 +150,8 @@ // Social login "social_login_list_title_continue" = "Continue with"; -"social_login_list_title_sign_in" = "Or login with"; -"social_login_list_title_sign_up" = "Or register with"; +"social_login_list_title_sign_in" = "Or"; +"social_login_list_title_sign_up" = "Or"; "social_login_button_title_continue" = "Continue with %@"; "social_login_button_title_sign_in" = "Sign In with %@"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 3a8e00d9b..0fef0ce78 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4330,11 +4330,11 @@ internal enum VectorL10n { internal static var socialLoginListTitleContinue: String { return VectorL10n.tr("Vector", "social_login_list_title_continue") } - /// Or login with + /// Or internal static var socialLoginListTitleSignIn: String { return VectorL10n.tr("Vector", "social_login_list_title_sign_in") } - /// Or register with + /// Or internal static var socialLoginListTitleSignUp: String { return VectorL10n.tr("Vector", "social_login_list_title_sign_up") } diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index c8fdf6219..75a36e244 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -244,7 +244,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Redirect NSLogs to files only if we are not debugging if (!isatty(STDERR_FILENO)) { - [MXLogger redirectNSLogToFiles:YES numberOfFiles:50]; + NSUInteger sizeLimit = 100 * 1024 * 1024; // 100MB + [MXLogger redirectNSLogToFiles:YES numberOfFiles:50 sizeLimit:sizeLimit]; } NSLog(@"[AppDelegate] initialize: Done"); @@ -1138,9 +1139,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [[NSNotificationCenter defaultCenter] postNotificationName:AppDelegateUniversalLinkDidChangeNotification object:nil]; } - if ([webURL.path isEqualToString:@"/"]) + if ([self handleServerProvionningLink:webURL]) { - return [self handleServerProvionningLink:webURL]; + return YES; } NSString *validateEmailSubmitTokenPath = @"validate/email/submitToken"; @@ -1247,6 +1248,10 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni NSLog(@"[AppDelegate] Universal link: handleUniversalLinkFragment: %@", fragment); + // Make sure we have plain utf8 character for separators + fragment = [fragment stringByRemovingPercentEncoding]; + NSLog(@"[AppDelegate] Universal link: handleUniversalLinkFragment: %@", fragment); + // The app manages only one universal link at a time // Discard any pending one [self resetPendingUniversalLink]; @@ -1368,9 +1373,27 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni withString:[MXTools encodeURIComponent:roomId] ]; - universalLinkFragmentPendingRoomAlias = @{roomId: roomIdOrAlias}; + // The previous operation can fail because of percent encoding + // TBH we are not clean on data inputs. For the moment, just give another try with no encoding + // TODO: Have a dedicated module and tests to handle universal links (matrix.to, email link, etc) + if ([newUniversalLinkFragment isEqualToString:fragment]) + { + newUniversalLinkFragment = + [fragment stringByReplacingOccurrencesOfString:roomIdOrAlias + withString:[MXTools encodeURIComponent:roomId]]; + } - [self handleUniversalLinkFragment:newUniversalLinkFragment]; + if (![newUniversalLinkFragment isEqualToString:fragment]) + { + universalLinkFragmentPendingRoomAlias = @{roomId: roomIdOrAlias}; + + [self handleUniversalLinkFragment:newUniversalLinkFragment]; + } + else + { + // Do not continue. Else we will loop forever + NSLog(@"[AppDelegate] Universal link: Error: Cannot resolve alias in %@ to the room id %@", fragment, roomId); + } } } failure:^(NSError *error) { @@ -1414,40 +1437,32 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // FIXME: In case of multi-account, ask the user which one to use MXKAccount* account = accountManager.activeAccounts.firstObject; - RoomPreviewData *roomPreviewData; + RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithRoomId:roomIdOrAlias + andSession:account.mxSession]; if (queryParams) { + roomPreviewData.viaServers = queryParams[@"via"]; + } + + // Is it a link to an event of a room? + // If yes, the event will be displayed once the room is joined + roomPreviewData.eventId = (pathParams.count >= 3) ? pathParams[2] : nil; + + // Try to get more information about the room before opening its preview + [roomPreviewData peekInRoom:^(BOOL succeeded) { + // Note: the activity indicator will not disappear if the session is not ready [homeViewController stopActivityIndicator]; - roomPreviewData = [[RoomPreviewData alloc] initWithRoomId:roomIdOrAlias emailInvitationParams:queryParams andSession:account.mxSession]; - roomPreviewData.viaServers = queryParams[@"via"]; + // If no data is available for this room, we name it with the known room alias (if any). + if (!succeeded && universalLinkFragmentPendingRoomAlias[roomIdOrAlias]) + { + roomPreviewData.roomName = universalLinkFragmentPendingRoomAlias[roomIdOrAlias]; + } + universalLinkFragmentPendingRoomAlias = nil; + [self showRoomPreview:roomPreviewData]; - } - else - { - roomPreviewData = [[RoomPreviewData alloc] initWithRoomId:roomIdOrAlias andSession:account.mxSession]; - - // Is it a link to an event of a room? - // If yes, the event will be displayed once the room is joined - roomPreviewData.eventId = (pathParams.count >= 3) ? pathParams[2] : nil; - - // Try to get more information about the room before opening its preview - [roomPreviewData peekInRoom:^(BOOL succeeded) { - - // Note: the activity indicator will not disappear if the session is not ready - [homeViewController stopActivityIndicator]; - - // If no data is available for this room, we name it with the known room alias (if any). - if (!succeeded && universalLinkFragmentPendingRoomAlias[roomIdOrAlias]) - { - roomPreviewData.roomName = universalLinkFragmentPendingRoomAlias[roomIdOrAlias]; - } - universalLinkFragmentPendingRoomAlias = nil; - - [self showRoomPreview:roomPreviewData]; - }]; - } + }]; } } @@ -2218,6 +2233,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni case MXSessionStateSyncInProgress: // Stay in launching during the first server sync if the store is empty. isLaunching = (mainSession.rooms.count == 0 && launchAnimationContainerView); + + if (mainSession.crypto.crossSigning && mainSession.crypto.crossSigning.state == MXCrossSigningStateCrossSigningExists) + { + [mainSession.crypto setOutgoingKeyRequestsEnabled:NO onComplete:nil]; + } break; default: isLaunching = NO; @@ -3486,81 +3506,100 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni return; } + MXWeakify(self); [mxSession.crypto pendingKeyRequests:^(MXUsersDevicesMap *> *pendingKeyRequests) { - - NSLog(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: pendingKeyRequests.count: %@. Already displayed: %@", + + MXStrongifyAndReturnIfNil(self); + NSLog(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: cross-signing state: %ld, pendingKeyRequests.count: %@. Already displayed: %@", + mxSession.crypto.crossSigning.state, @(pendingKeyRequests.count), - roomKeyRequestViewController ? @"YES" : @"NO"); + self->roomKeyRequestViewController ? @"YES" : @"NO"); - if (roomKeyRequestViewController) + if (!mxSession.crypto.crossSigning || mxSession.crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped) { - // Check if the current RoomKeyRequestViewController is still valid - MXSession *currentMXSession = roomKeyRequestViewController.mxSession; - NSString *currentUser = roomKeyRequestViewController.device.userId; - NSString *currentDevice = roomKeyRequestViewController.device.deviceId; - - NSArray *currentPendingRequest = [pendingKeyRequests objectForDevice:currentDevice forUser:currentUser]; - - if (currentMXSession == mxSession && currentPendingRequest.count == 0) + if (self->roomKeyRequestViewController) { - NSLog(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Cancel current dialog"); + // Check if the current RoomKeyRequestViewController is still valid + MXSession *currentMXSession = self->roomKeyRequestViewController.mxSession; + NSString *currentUser = self->roomKeyRequestViewController.device.userId; + NSString *currentDevice = self->roomKeyRequestViewController.device.deviceId; - // The key request has been probably cancelled, remove the popup - [roomKeyRequestViewController hide]; - roomKeyRequestViewController = nil; + NSArray *currentPendingRequest = [pendingKeyRequests objectForDevice:currentDevice forUser:currentUser]; + + if (currentMXSession == mxSession && currentPendingRequest.count == 0) + { + NSLog(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Cancel current dialog"); + + // The key request has been probably cancelled, remove the popup + [self->roomKeyRequestViewController hide]; + self->roomKeyRequestViewController = nil; + } } } - if (!roomKeyRequestViewController && pendingKeyRequests.count) + if (!self->roomKeyRequestViewController && pendingKeyRequests.count) { // Pick the first coming user/device pair NSString *userId = pendingKeyRequests.userIds.firstObject; NSString *deviceId = [pendingKeyRequests deviceIdsForUser:userId].firstObject; - + // Give the client a chance to refresh the device list + MXWeakify(self); [mxSession.crypto downloadKeys:@[userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) { - + + MXStrongifyAndReturnIfNil(self); MXDeviceInfo *deviceInfo = [usersDevicesInfoMap objectForDevice:deviceId forUser:userId]; if (deviceInfo) { - BOOL wasNewDevice = (deviceInfo.trustLevel.localVerificationStatus == MXDeviceUnknown); - - void (^openDialog)(void) = ^void() + if (!mxSession.crypto.crossSigning || mxSession.crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped) { - NSLog(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Open dialog for %@", deviceInfo); + BOOL wasNewDevice = (deviceInfo.trustLevel.localVerificationStatus == MXDeviceUnknown); + + void (^openDialog)(void) = ^void() + { + NSLog(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Open dialog for %@", deviceInfo); - roomKeyRequestViewController = [[RoomKeyRequestViewController alloc] initWithDeviceInfo:deviceInfo wasNewDevice:wasNewDevice andMatrixSession:mxSession onComplete:^{ + self->roomKeyRequestViewController = [[RoomKeyRequestViewController alloc] initWithDeviceInfo:deviceInfo wasNewDevice:wasNewDevice andMatrixSession:mxSession onComplete:^{ - roomKeyRequestViewController = nil; + self->roomKeyRequestViewController = nil; - // Check next pending key request, if any + // Check next pending key request, if any + [self checkPendingRoomKeyRequests]; + }]; + + [self->roomKeyRequestViewController show]; + }; + + // If the device was new before, it's not any more. + if (wasNewDevice) + { + [mxSession.crypto setDeviceVerification:MXDeviceUnverified forDevice:deviceId ofUser:userId success:openDialog failure:nil]; + } + else + { + openDialog(); + } + } + else if (deviceInfo.trustLevel.isVerified) + { + [mxSession.crypto acceptAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ [self checkPendingRoomKeyRequests]; }]; - - [roomKeyRequestViewController show]; - }; - - // If the device was new before, it's not any more. - if (wasNewDevice) - { - [mxSession.crypto setDeviceVerification:MXDeviceUnverified forDevice:deviceId ofUser:userId success:openDialog failure:nil]; } else { - openDialog(); + [mxSession.crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ + [self checkPendingRoomKeyRequests]; + }]; } } else { NSLog(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: No details found for device %@:%@", userId, deviceId); - - // Ignore this device to avoid to loop on it [mxSession.crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ - // And check next requests [self checkPendingRoomKeyRequests]; }]; } - } failure:^(NSError *error) { // Retry later NSLog(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Failed to download device keys. Retry"); @@ -3732,6 +3771,12 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni - (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId { + MXCrypto *crypto = coordinatorBridgePresenter.session.crypto; + if (!crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled) + { + NSLog(@"[AppDelegate][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys"); + [crypto setOutgoingKeyRequestsEnabled:YES onComplete:nil]; + } [self dismissKeyVerificationCoordinatorBridgePresenter]; } diff --git a/Riot/Modules/AuthenticatedSession/AuthenticatedSessionViewControllerFactory.swift b/Riot/Modules/AuthenticatedSession/AuthenticatedSessionViewControllerFactory.swift index a62f57cd9..b9ccc22f9 100644 --- a/Riot/Modules/AuthenticatedSession/AuthenticatedSessionViewControllerFactory.swift +++ b/Riot/Modules/AuthenticatedSession/AuthenticatedSessionViewControllerFactory.swift @@ -15,167 +15,24 @@ */ import Foundation -enum AuthenticatedSessionViewControllerFactoryError: Int, Error, CustomNSError { - case flowNotSupported = 0 - - // MARK: - CustomNSError - - static let errorDomain = "AuthenticatedSessionViewControllerFactoryErrorDomain" - - var errorCode: Int { return self.rawValue } - - var errorUserInfo: [String: Any] { - let userInfo: [String: Any] - - switch self { - case .flowNotSupported: - userInfo = [NSLocalizedDescriptionKey: VectorL10n.authenticatedSessionFlowNotSupported] - } - return userInfo - } -} - /// This class creates view controllers that can handle an authentication flow for given requests. @objcMembers final class AuthenticatedSessionViewControllerFactory: NSObject { - - // MARK: - Constants - - // MARK: - Properties - - // MARK: Private - - private let session: MXSession - - - // MARK: Public - - // MARK: - Setup - - init(session: MXSession) { - self.session = session - } - - - // MARK: - Public methods - - /// Create a view controller to handle an authentication flow for a given request. - /// - /// - Parameters: - /// - path: the request path. - /// - httpMethod: the request http method. - /// - title: the title to use in the view controller. - /// - message: the information to display in the view controller. - /// - onViewController: the block called when the view controller is ready. The caller must display it. - /// - onAuthenticated: the block called when the user finished to enter their credentials. - /// - onCancelled: the block called when the user cancelled the authentication. - /// - onFailure: the blocked called on error. - @discardableResult - func viewController(forPath path: String, - httpMethod: String, - title: String?, - message: String?, - onViewController: @escaping (UIViewController) -> Void, - onAuthenticated: @escaping ([String: Any]) -> Void, - onCancelled: @escaping () -> Void, - onFailure: @escaping (Error) -> Void) -> MXHTTPOperation { - // Get the authentication flow required for this API - return session.matrixRestClient.authSessionForRequest(withMethod: httpMethod, path: path, parameters: [:], success: { [weak self] (authenticationSession) in - guard let self = self else { - return - } - - guard let authenticationSession = authenticationSession, let flows = authenticationSession.flows else { - onFailure(AuthenticatedSessionViewControllerFactoryError.flowNotSupported) - return - } - - // Return the corresponding VC - if self.hasPasswordFlow(inFlows: flows) { - let authViewController = self.createPasswordViewController(title: title, - message: message, - authenticationSession: authenticationSession, - onAuthenticated: onAuthenticated, - onCancelled: onCancelled, - onFailure: onFailure) - onViewController(authViewController) - } else { - // Flow not supported yet - onFailure(AuthenticatedSessionViewControllerFactoryError.flowNotSupported) - } - - }, failure: { (error) in - guard let error = error else { - return - } - - onFailure(error) - }) - } - - /// Check if we support the authentication flow for a given request. - /// - /// - Parameters: - /// - path: the request path. - /// - httpMethod: the request http method. - /// - onCancelled: the block called when the user cancelled the authentication. - /// - onFailure: the blocked called on error. - func hasSupport(forPath path: String, - httpMethod: String, - success: @escaping (Bool) -> Void, - failure: @escaping (Error) -> Void) -> MXHTTPOperation { - - // Get the authentication flow required for this API - return session.matrixRestClient.authSessionForRequest(withMethod: httpMethod, path: path, parameters: [:], success: { [weak self] (authenticationSession) in - guard let self = self else { - return - } - - guard let authenticationSession = authenticationSession, let flows = authenticationSession.flows else { - success(false) - return - } - - // Return the corresponding VC - if self.hasPasswordFlow(inFlows: flows) { - success(true) - } else { - // Flow not supported yet - success(false) - } - - }, failure: { (error) in - guard let error = error else { - return - } - - failure(error) - }) - } - - - // MARK: - Private methods - // MARK: - Password flow - private func hasPasswordFlow(inFlows flows: [MXLoginFlow]) -> Bool { - for flow in flows { - if flow.type == kMXLoginFlowTypePassword || flow.stages.contains(kMXLoginFlowTypePassword) { - return true - } - } - - return false - } - - private func createPasswordViewController( + /// Create a view controller to handle a password authentication. + /// - Parameters: + /// - title: the title to use in the view controller. + /// - message: the information to display in the view controller. + /// - onPasswordEntered: the closure called when the enter the password. + /// - onCancelled: the closure called when the user cancelled the authentication. + /// - Returns: the password authentication view controller + func createPasswordViewController( title: String?, message: String?, - authenticationSession: MXAuthenticationSession, - onAuthenticated: @escaping ([String: Any]) -> Void, - onCancelled: @escaping () -> Void, - onFailure: @escaping (Error) -> Void) -> UIViewController { + onPasswordEntered: @escaping (String) -> Void, + onCancelled: @escaping () -> Void) -> UIViewController { // Use a simple UIAlertController as before let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) @@ -196,29 +53,9 @@ final class AuthenticatedSessionViewControllerFactory: NSObject { // Should not happen return } - - guard let authParams = self.createAuthParams(password: password, authenticationSession: authenticationSession) else { - onFailure(AuthenticatedSessionViewControllerFactoryError.flowNotSupported) - return - } - - onAuthenticated(authParams) + onPasswordEntered(password) })) return alertController } - - private func createAuthParams(password: String, - authenticationSession: MXAuthenticationSession) -> [String: Any]? { - guard let userId = self.session.myUserId, let session = authenticationSession.session else { - return nil - } - - return [ - "type": kMXLoginFlowTypePassword, - "session": session, - "user": userId, - "password": password - ] - } } diff --git a/Riot/Modules/AuthenticatedSession/AuthenticationParametersBuilder.swift b/Riot/Modules/AuthenticatedSession/AuthenticationParametersBuilder.swift new file mode 100644 index 000000000..d600e6b66 --- /dev/null +++ b/Riot/Modules/AuthenticatedSession/AuthenticationParametersBuilder.swift @@ -0,0 +1,45 @@ +// +// Copyright 2020 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 Foundation + +/// Build authentication parameters depending on login type +final class AuthenticationParametersBuilder { + + func buildPasswordParameters(sessionId: String, + userId: String, + password: String) -> [String: Any]? { + return [ + "type": MXLoginFlowType.password.identifier, + "session": sessionId, + "user": userId, + "password": password + ] + } + + func buildTokenParameters(with loginToken: String) -> [String: Any] { + return [ + "type": MXLoginFlowType.token.identifier, + "token": loginToken + ] + } + + func buildOAuthParameters(with sessionId: String) -> [String: Any] { + return [ + "session": sessionId + ] + } +} diff --git a/Riot/Modules/AuthenticatedSession/AuthenticationSessionParameters.swift b/Riot/Modules/AuthenticatedSession/AuthenticationSessionParameters.swift new file mode 100644 index 000000000..97a8e344b --- /dev/null +++ b/Riot/Modules/AuthenticatedSession/AuthenticationSessionParameters.swift @@ -0,0 +1,31 @@ +// +// Copyright 2020 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 Foundation + +/// AuthenticationSessionParameters represents authenticated API endpoint parameters. +@objcMembers +class AuthenticationSessionParameters: NSObject { + + let path: String + let httpMethod: String + + init(path: String, httpMethod: String) { + self.path = path + self.httpMethod = httpMethod + super.init() + } +} diff --git a/Riot/Modules/AuthenticatedSession/AuthenticationSessionService.swift b/Riot/Modules/AuthenticatedSession/AuthenticationSessionService.swift new file mode 100644 index 000000000..379f2d2c7 --- /dev/null +++ b/Riot/Modules/AuthenticatedSession/AuthenticationSessionService.swift @@ -0,0 +1,246 @@ +// +// Copyright 2020 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 Foundation + +/// AuthenticationSessionService errors +enum AuthenticationSessionServiceError: Int, Error { + case flowNotSupported = 0 +} + +extension AuthenticationSessionServiceError: CustomNSError { + static let errorDomain = "AuthenticationSessionServiceErrorDomain" + + var errorCode: Int { + return self.rawValue + } + + var errorUserInfo: [String: Any] { + let userInfo: [String: Any] + + switch self { + case .flowNotSupported: + userInfo = [NSLocalizedDescriptionKey: VectorL10n.authenticatedSessionFlowNotSupported] + } + return userInfo + } +} + +/// AuthenticationSessionResult indicates the success state after checking if an API endpoint needs authentication +enum AuthenticationSessionResult { + case authenticationNotNeeded + case authenticationNeeded(_ authenticationSession: MXAuthenticationSession) +} + +/// AuthenticationSessionService enables to check if an API endpoint needs authentication. +@objcMembers +final class AuthenticationSessionService: NSObject { + + // MARK: - Constants + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + + // MARK: Public + + // MARK: - Setup + + init(session: MXSession) { + self.session = session + super.init() + } + + // MARK: - Public + + /// Check if API endpoint requires authentication and get authentication session if needed + /// - Parameters: + /// - authenticationSessionParameters: API endpoint parameters + /// - completion: The closure executed the operation completes. AuthenticationSessionResult indicates + /// - Returns: A `MXHTTPOperation` instance. + @discardableResult + func authenticationSession(for authenticationSessionParameters: AuthenticationSessionParameters, completion: @escaping (Result) -> Void) -> MXHTTPOperation { + + return self.authenticationSession(for: authenticationSessionParameters) { (authenticationSession) in + + let authenticationResult: AuthenticationSessionResult + + if let authenticationSession = authenticationSession { + authenticationResult = .authenticationNeeded(authenticationSession) + } else { + authenticationResult = .authenticationNotNeeded + } + + completion(.success(authenticationResult)) + } failure: { (error) in + completion(.failure(error)) + } + } + + /// Check if API endpoint requires authentication. + /// - Parameters: + /// - authenticationSessionParameters: API endpoint parameters + /// - completion: The closure executed the operation completes. + /// - Returns: A `MXHTTPOperation` instance. + @discardableResult + func canAuthenticate(for authenticationSessionParameters: AuthenticationSessionParameters, completion: @escaping (Result) -> Void) -> MXHTTPOperation { + + return self.authenticationSession(for: authenticationSessionParameters) { (result) in + switch result { + case .success(let authenticationSessionResult): + let canAuthenticate: Bool + + switch authenticationSessionResult { + case .authenticationNotNeeded: + canAuthenticate = true + case .authenticationNeeded(let authenticationSession): + canAuthenticate = self.canAuthenticate(with: authenticationSession) + } + + completion(.success(canAuthenticate)) + case .failure(let error): + completion(.failure(error)) + } + } + } + + /// Check if API endpoint requires authentication. + /// This method is compatible with Objective-C + /// - Parameters: + /// - authenticationSessionParameters: API endpoint parameters + /// - success: The closure executed the operation succeed. Get an MXAuthenticationSession if authentication is required or nil if there is no authentication needed. + /// - failure: The closure executed the operation fails. + /// - Returns: A `MXHTTPOperation` instance. + @discardableResult + func authenticationSession(for authenticationSessionParameters: AuthenticationSessionParameters, + success: @escaping (MXAuthenticationSession?) -> Void, + failure: @escaping (Error) -> Void) -> MXHTTPOperation { + // Get the authentication flow required for this API + return self.session.matrixRestClient.authSessionForRequest(withMethod: authenticationSessionParameters.httpMethod, path: authenticationSessionParameters.path, parameters: [:], success: { [weak self] (authenticationSession) in + guard let self = self else { + return + } + + if let authenticationSession = authenticationSession { + + if let flows = authenticationSession.flows { + // Check if a supported flow exists + if self.isThereAKnownFlow(inFlows: flows) { + success(authenticationSession) + } else if self.firstUncompletedStageAuthenticationFallbackURL(for: authenticationSession) != nil { + success(authenticationSession) + } else { + // Flow not supported + failure(AuthenticationSessionServiceError.flowNotSupported) + } + } + } else { + // No need to authenticate + success(nil) + } + }, failure: { (error) in + guard let error = error else { + return + } + failure(error) + }) + } + + /// Get the authentication fallback URL for the first uncompleted stage found. + /// - Parameter authenticationSession: An authentication session for a given request. + /// - Returns: The fallback URL for the first uncompleted stage found. + func firstUncompletedStageAuthenticationFallbackURL(for authenticationSession: MXAuthenticationSession) -> URL? { + guard let sessiondId = authenticationSession.session, let firstUncompletedStageIdentifier = self.firstUncompletedFlowIdentifier(in: authenticationSession) else { + return nil + } + return self.authenticationFallbackURL(for: firstUncompletedStageIdentifier, sessionId: sessiondId) + } + + /// Build UIA fallback authentication URL for a given stage (https://matrix.org/docs/spec/client_server/latest#fallback) + /// - Parameters: + /// - flowIdentifier: The login type to authenticate with. + /// - sessionId: The the ID of the session given by the homeserver. + /// - Returns: a `MXHTTPOperation` instance. + func authenticationFallbackURL(for flowIdentifier: String, sessionId: String) -> URL? { + guard let homeserverStringURL = self.session.matrixRestClient.credentials.homeServer, let homeserverURL = URL(string: homeserverStringURL) else { + return nil + } + + let fallbackPath = "\(kMXAPIPrefixPathR0)/auth/\(flowIdentifier)/fallback/web?session=\(sessionId)" + + return URL(string: fallbackPath, relativeTo: homeserverURL) + } + + /// Find the first uncompleted login flow stage in a MXauthenticationSession. + /// - Parameter authenticationSession: An authentication session for a given request. + /// - Returns: Uncompleted login flow stage identifier. + func firstUncompletedFlowIdentifier(in authenticationSession: MXAuthenticationSession) -> String? { + + let completedStages = authenticationSession.completed ?? [] + + guard let flows = authenticationSession.flows else { + return nil + } + + // Remove nil values + let allNonNullStages = flows.compactMap { $0.stages } + + // Make a flat array of all stages + let allStages: [String] = allNonNullStages.flatMap { $0 } + + // Keep stages order + let uncompletedStages = NSMutableOrderedSet(array: allStages) + + // Keep uncompleted stages + let completedStagesSet = NSOrderedSet(array: completedStages) + uncompletedStages.minus(completedStagesSet) + + let firstUncompletedFlowIdentifier = uncompletedStages.firstObject as? String + return firstUncompletedFlowIdentifier + } + + /// Check if an array of login flows contains "m.login.password" flow. + func hasPasswordFlow(inFlows flows: [MXLoginFlow]) -> Bool { + for flow in flows { + if flow.type == MXLoginFlowType.password.identifier || flow.stages.contains(MXLoginFlowType.password.identifier) { + return true + } + } + + return false + } + + // MARK: - Private + + private func isThereAKnownFlow(inFlows flows: [MXLoginFlow]) -> Bool { + return self.hasPasswordFlow(inFlows: flows) + } + + private func canAuthenticate(with authenticationSession: MXAuthenticationSession) -> Bool { + + if self.isThereAKnownFlow(inFlows: authenticationSession.flows) { + return true + } + + if self.firstUncompletedStageAuthenticationFallbackURL(for: authenticationSession) != nil { + return true + } + + return false + } +} diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m index c44b79f92..f769c5e48 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -1398,18 +1398,23 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; - (void)sessionStateDidChangeNotification:(NSNotification*)notification { MXSession *session = (MXSession*)notification.object; - - if (session.state == MXSessionStateRunning) + + if (session.state == MXSessionStateStoreDataReady) { - [self unregisterSessionStateChangeNotification]; - if (session.crypto.crossSigning) { // Do not make key share requests while the "Complete security" is not complete. // If the device is self-verified, the SDK will restore the existing key backup. // Then, it will re-enable outgoing key share requests [session.crypto setOutgoingKeyRequestsEnabled:NO onComplete:nil]; - + } + } + else if (session.state == MXSessionStateRunning) + { + [self unregisterSessionStateChangeNotification]; + + if (session.crypto.crossSigning) + { [session.crypto.crossSigning refreshStateWithSuccess:^(BOOL stateUpdated) { NSLog(@"[AuthenticationVC] sessionStateDidChange: crossSigning.state: %@", @(session.crypto.crossSigning.state)); @@ -1619,14 +1624,17 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; - (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId { + MXCrypto *crypto = coordinatorBridgePresenter.session.crypto; + if (!crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled) + { + NSLog(@"[AuthenticationVC][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys"); + [crypto setOutgoingKeyRequestsEnabled:YES onComplete:nil]; + } [self dismiss]; } - (void)keyVerificationCoordinatorBridgePresenterDelegateDidCancel:(KeyVerificationCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter { - // Set outgoing key requests back - [coordinatorBridgePresenter.session.crypto setOutgoingKeyRequestsEnabled:YES onComplete:nil]; - [self dismiss]; } diff --git a/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift b/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift index d4baee66b..21084d92a 100644 --- a/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift +++ b/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift @@ -68,11 +68,17 @@ final class SSOAuthenticationPresenter: NSObject { self.presentingViewController = presentingViewController - // SFAuthenticationSession and ASWebAuthenticationSession doesn't work with guided access (rdar://48376122) - if UIAccessibility.isGuidedAccessEnabled { - self.presentSafariViewController(with: authenticationURL, animated: animated) + // NOTE: By using SFAuthenticationSession the consent alert show product name instead of display name. Fallback to SFSafariViewController instead in order to not disturb users with "Riot" wording at the moment. + // (https://stackoverflow.com/questions/49860338/why-does-sfauthenticationsession-consent-alert-show-xcode-project-name-instead-o) + if #available(iOS 13.0, *) { + // SFAuthenticationSession and ASWebAuthenticationSession doesn't work with guided access (rdar://48376122) + if UIAccessibility.isGuidedAccessEnabled { + self.presentSafariViewController(with: authenticationURL, animated: animated) + } else { + self.startAuthenticationSession(with: authenticationURL) + } } else { - self.startAuthenticationSession(with: authenticationURL) + self.presentSafariViewController(with: authenticationURL, animated: animated) } } diff --git a/Riot/Modules/CrossSigning/CrossSigningService.swift b/Riot/Modules/CrossSigning/CrossSigningService.swift index 1168a8243..8140a55c6 100644 --- a/Riot/Modules/CrossSigning/CrossSigningService.swift +++ b/Riot/Modules/CrossSigning/CrossSigningService.swift @@ -19,8 +19,12 @@ import Foundation @objcMembers final class CrossSigningService: NSObject { - private var authenticatedSessionFactory: AuthenticatedSessionViewControllerFactory? + // MARK - Properties + private var supportSetupKeyVerificationByUser: [String: Bool] = [:] // Cached server response + private var authenticationSessionService: AuthenticationSessionService? + + // MARK - Public @discardableResult func canSetupCrossSigning(for session: MXSession, success: @escaping ((Bool) -> Void), failure: @escaping ((Error) -> Void)) -> MXHTTPOperation? { @@ -39,25 +43,24 @@ final class CrossSigningService: NSObject { return nil } - let authenticatedSessionFactory = AuthenticatedSessionViewControllerFactory(session: session) + let authenticationSessionService = AuthenticationSessionService(session: session) - self.authenticatedSessionFactory = authenticatedSessionFactory + self.authenticationSessionService = authenticationSessionService + + let authenticationSessionParameters = self.setupCrossSigningAuthenticationSessionParameters() - let path = "\(kMXAPIPrefixPathUnstable)/keys/device_signing/upload" - - return authenticatedSessionFactory.hasSupport(forPath: path, httpMethod: "POST", success: { [weak self] succeeded in - guard let self = self else { - return - } - self.authenticatedSessionFactory = nil - self.supportSetupKeyVerificationByUser[userId] = succeeded - success(succeeded) - }, failure: { [weak self] error in - guard let self = self else { - return - } - self.authenticatedSessionFactory = nil + return authenticationSessionService.canAuthenticate(for: authenticationSessionParameters) { (result) in + switch result { + case .success(let succeeded): + success(succeeded) + case .failure(let error): failure(error) - }) + } + } + } + + func setupCrossSigningAuthenticationSessionParameters() -> AuthenticationSessionParameters { + let path = "\(kMXAPIPrefixPathUnstable)/keys/device_signing/upload" + return AuthenticationSessionParameters(path: path, httpMethod: "POST") } } diff --git a/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinator.swift b/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinator.swift new file mode 100644 index 000000000..05d5f1d4c --- /dev/null +++ b/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinator.swift @@ -0,0 +1,107 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh CrossSigning CrossSigningSetup +/* + 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 UIKit + +@objcMembers +final class CrossSigningSetupCoordinator: CrossSigningSetupCoordinatorType { + + // MARK: - Properties + + // MARK: Private + + private let parameters: CrossSigningSetupCoordinatorParameters + private let crossSigningService: CrossSigningService + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: CrossSigningSetupCoordinatorDelegate? + + // MARK: - Setup + + init(parameters: CrossSigningSetupCoordinatorParameters) { + self.parameters = parameters + self.crossSigningService = CrossSigningService() + } + + // MARK: - Public methods + + func start() { + self.showReauthentication() + } + + func toPresentable() -> UIViewController { + return self.parameters.presenter.toPresentable() + } + + // MARK: - Private methods + + private func showReauthentication() { + + let authenticationSessionParameters = self.crossSigningService.setupCrossSigningAuthenticationSessionParameters() + + let reauthenticationParameters = ReauthenticationCoordinatorParameters(session: parameters.session, + presenter: parameters.presenter, + title: parameters.title, + message: parameters.message, + authenticationSessionParameters: authenticationSessionParameters) + + let coordinator = ReauthenticationCoordinator(parameters: reauthenticationParameters) + coordinator.delegate = self + self.add(childCoordinator: coordinator) + + coordinator.start() + } + + private func setupCrossSigning(with authenticationParameters: [String: Any]) { + guard let crossSigning = self.parameters.session.crypto.crossSigning else { + return + } + + crossSigning.setup(withAuthParams: authenticationParameters) { [weak self] in + guard let self = self else { + return + } + self.delegate?.crossSigningSetupCoordinatorDidComplete(self) + } failure: { [weak self] error in + guard let self = self else { + return + } + self.delegate?.crossSigningSetupCoordinator(self, didFailWithError: error) + } + } +} + +// MARK: - ReauthenticationCoordinatorDelegate +extension CrossSigningSetupCoordinator: ReauthenticationCoordinatorDelegate { + + func reauthenticationCoordinatorDidComplete(_ coordinator: ReauthenticationCoordinatorType, withAuthenticationParameters authenticationParameters: [String: Any]?) { + self.setupCrossSigning(with: authenticationParameters ?? [:]) + } + + func reauthenticationCoordinatorDidCancel(_ coordinator: ReauthenticationCoordinatorType) { + self.delegate?.crossSigningSetupCoordinatorDidCancel(self) + } + + func reauthenticationCoordinator(_ coordinator: ReauthenticationCoordinatorType, didFailWithError error: Error) { + self.delegate?.crossSigningSetupCoordinator(self, didFailWithError: error) + } +} diff --git a/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinatorBridgePresenter.swift b/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinatorBridgePresenter.swift new file mode 100644 index 000000000..b334c9c40 --- /dev/null +++ b/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinatorBridgePresenter.swift @@ -0,0 +1,105 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh CrossSigning CrossSigningSetup +/* + 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 Foundation + +/// CrossSigningSetupCoordinatorBridgePresenter enables to start CrossSigningSetupCoordinator from a view controller. +/// This bridge is used while waiting for global usage of coordinator pattern. +/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (mainly for integration in legacy view controllers). +/// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. +@objcMembers +final class CrossSigningSetupCoordinatorBridgePresenter: NSObject { + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + private var coordinator: CrossSigningSetupCoordinator? + + private var didComplete: (() -> Void)? + private var didCancel: (() -> Void)? + private var didFail: ((Error) -> Void)? + + // MARK: - Setup + + init(session: MXSession) { + self.session = session + super.init() + } + + // MARK: - Public + + func present(with title: String, + message: String, + from viewController: UIViewController, + animated: Bool, + success: @escaping () -> Void, + cancel: @escaping () -> Void, + failure: @escaping (Error) -> Void) { + + self.didComplete = success + self.didCancel = cancel + self.didFail = failure + + let parameters = CrossSigningSetupCoordinatorParameters(session: self.session, presenter: viewController, title: title, message: message) + + let crossSigningSetupCoordinator = CrossSigningSetupCoordinator(parameters: parameters) + crossSigningSetupCoordinator.delegate = self + crossSigningSetupCoordinator.start() + + self.coordinator = crossSigningSetupCoordinator + } + + func dismiss(animated: Bool, completion: (() -> Void)?) { + + self.resetCompletions() + + guard let coordinator = self.coordinator else { + return + } + coordinator.toPresentable().dismiss(animated: animated) { + self.coordinator = nil + + if let completion = completion { + completion() + } + } + } + + private func resetCompletions() { + self.didComplete = nil + self.didCancel = nil + self.didFail = nil + } +} + +// MARK: - CrossSigningSetupCoordinatorDelegate +extension CrossSigningSetupCoordinatorBridgePresenter: CrossSigningSetupCoordinatorDelegate { + func crossSigningSetupCoordinatorDidComplete(_ coordinator: CrossSigningSetupCoordinatorType) { + self.didComplete?() + } + + func crossSigningSetupCoordinatorDidCancel(_ coordinator: CrossSigningSetupCoordinatorType) { + self.didCancel?() + } + + func crossSigningSetupCoordinator(_ coordinator: CrossSigningSetupCoordinatorType, didFailWithError error: Error) { + self.didFail?(error) + } +} diff --git a/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinatorParameters.swift b/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinatorParameters.swift new file mode 100644 index 000000000..65b50f2d9 --- /dev/null +++ b/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinatorParameters.swift @@ -0,0 +1,46 @@ +// +// Copyright 2020 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 Foundation + +/// CrossSigningSetupCoordinator input parameters +@objcMembers +class CrossSigningSetupCoordinatorParameters: NSObject { + + /// The Matrix session + let session: MXSession + + /// The presenter used to show authentication screen(s) + let presenter: Presentable + + /// The title to use in the authentication screen if present. + let title: String? + + /// The message to use in the authentication screen if present. + let message: String? + + init(session: MXSession, + presenter: Presentable, + title: String?, + message: String?) { + self.session = session + self.presenter = presenter + self.title = title + self.message = message + + super.init() + } +} diff --git a/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinatorType.swift b/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinatorType.swift new file mode 100644 index 000000000..1dc620d3f --- /dev/null +++ b/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinatorType.swift @@ -0,0 +1,30 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh CrossSigning CrossSigningSetup +/* + Copyright 2020 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 Foundation + +protocol CrossSigningSetupCoordinatorDelegate: class { + func crossSigningSetupCoordinatorDidComplete(_ coordinator: CrossSigningSetupCoordinatorType) + func crossSigningSetupCoordinatorDidCancel(_ coordinator: CrossSigningSetupCoordinatorType) + func crossSigningSetupCoordinator(_ coordinator: CrossSigningSetupCoordinatorType, didFailWithError error: Error) +} + +/// `CrossSigningSetupCoordinatorType` is a protocol describing a Coordinator that handles cross signing setup navigation flow. +protocol CrossSigningSetupCoordinatorType: Coordinator, Presentable { + var delegate: CrossSigningSetupCoordinatorDelegate? { get } +} diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index 95bc66b7a..8a5633156 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -44,7 +44,7 @@ @property (nonatomic, strong) SecureBackupBannerCell *secureBackupBannerPrototypeCell; @property (nonatomic, strong) CrossSigningSetupBannerCell *keyVerificationSetupBannerPrototypeCell; -@property (nonatomic, strong) AuthenticatedSessionViewControllerFactory *authenticatedSessionViewControllerFactory; +@property (nonatomic, strong) CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter; @end @@ -736,68 +736,47 @@ message:(NSString*)message success:(void (^)(void))success failure:(void (^)(NSError *error))failure + { - __block UIViewController *viewController; [self startActivityIndicator]; self.view.userInteractionEnabled = NO; + MXWeakify(self); + void (^animationCompletion)(void) = ^void () { + MXStrongifyAndReturnIfNil(self); + [self stopActivityIndicator]; self.view.userInteractionEnabled = YES; + [self.crossSigningSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:^{}]; + self.crossSigningSetupCoordinatorBridgePresenter = nil; }; - // Get credentials to set up cross-signing - NSString *path = [NSString stringWithFormat:@"%@/keys/device_signing/upload", kMXAPIPrefixPathUnstable]; - self.authenticatedSessionViewControllerFactory = [[AuthenticatedSessionViewControllerFactory alloc] initWithSession:self.mainSession]; - [self.authenticatedSessionViewControllerFactory viewControllerForPath:path - httpMethod:@"POST" - title:title - message:message - onViewController:^(UIViewController * _Nonnull theViewController) - { - viewController = theViewController; - [self presentViewController:viewController animated:YES completion:nil]; - - } onAuthenticated:^(NSDictionary * _Nonnull authParams) { - - [viewController dismissViewControllerAnimated:NO completion:nil]; - viewController = nil; - - MXCrossSigning *crossSigning = self.mainSession.crypto.crossSigning; - if (crossSigning) - { - [crossSigning setupWithAuthParams:authParams success:^{ - animationCompletion(); - - // TODO: Remove this line and refresh key verification setup banner by listening to a local notification cross-signing state change (Add this behavior into the SDK). - [self->recentsDataSource setDelegate:self andRecentsDataSourceMode:RecentsDataSourceModeHome]; - - [self refreshRecentsTable]; - success(); - } failure:^(NSError * _Nonnull error) { - animationCompletion(); - [self refreshRecentsTable]; - - [[AppDelegate theDelegate] showErrorAsAlert:error]; - failure(error); - }]; - } - - } onCancelled:^{ - animationCompletion(); - - [viewController dismissViewControllerAnimated:NO completion:nil]; - viewController = nil; - failure(nil); - } onFailure:^(NSError * _Nonnull error) { - - animationCompletion(); - [[AppDelegate theDelegate] showErrorAsAlert:error]; - - [viewController dismissViewControllerAnimated:NO completion:nil]; - viewController = nil; - failure(error); - }]; + CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter = [[CrossSigningSetupCoordinatorBridgePresenter alloc] initWithSession:self.mainSession]; + + [crossSigningSetupCoordinatorBridgePresenter presentWith:title + message:message + from:self + animated:YES + success:^{ + animationCompletion(); + + // TODO: Remove this line and refresh key verification setup banner by listening to a local notification cross-signing state change (Add this behavior into the SDK). + [self->recentsDataSource setDelegate:self andRecentsDataSourceMode:RecentsDataSourceModeHome]; + [self refreshRecentsTable]; + + success(); + } cancel:^{ + animationCompletion(); + failure(nil); + } failure:^(NSError * _Nonnull error) { + animationCompletion(); + [self refreshRecentsTable]; + [[AppDelegate theDelegate] showErrorAsAlert:error]; + failure(error); + }]; + + self.crossSigningSetupCoordinatorBridgePresenter = crossSigningSetupCoordinatorBridgePresenter; } #pragma mark - Empty view management diff --git a/Riot/Modules/KeyVerification/Common/KeyVerificationService.swift b/Riot/Modules/KeyVerification/Common/KeyVerificationService.swift index aeca8a37d..29368c83f 100644 --- a/Riot/Modules/KeyVerification/Common/KeyVerificationService.swift +++ b/Riot/Modules/KeyVerification/Common/KeyVerificationService.swift @@ -19,8 +19,7 @@ import Foundation final class KeyVerificationService { private let cameraAccessManager: CameraAccessManager - - private var authenticatedSessionFactory: AuthenticatedSessionViewControllerFactory? + private var supportSetupKeyVerificationByUser: [String: Bool] = [:] // Cached server response init() { diff --git a/Riot/Modules/Reauthentication/ReauthFallBackViewController.swift b/Riot/Modules/Reauthentication/ReauthFallBackViewController.swift new file mode 100644 index 000000000..130e5a862 --- /dev/null +++ b/Riot/Modules/Reauthentication/ReauthFallBackViewController.swift @@ -0,0 +1,78 @@ +// +// 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 UIKit + +/// View controller used for User-Interactive Authentication fallback (https://matrix.org/docs/spec/client_server/latest#fallback) +final class ReauthFallBackViewController: AuthFallBackViewController, Themable { + + // MARK: - Properties + + // MARK: Public + + var didValidate: (() -> Void)? + var didCancel: (() -> Void)? + + // MARK: Private + + private var theme: Theme = ThemeService.shared().theme + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + self.setupNavigationBar() + self.registerThemeServiceDidChangeThemeNotification() + self.update(theme: self.theme) + } + + // MARK: - Public + + func update(theme: Theme) { + self.theme = theme + + if let navigationBar = self.navigationController?.navigationBar { + theme.applyStyle(onNavigationBar: navigationBar) + } + } + + // MARK: - Private + + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) + } + + @objc private func themeDidChange() { + self.update(theme: ThemeService.shared().theme) + } + + private func setupNavigationBar() { + let doneBarButtonItem = MXKBarButtonItem(title: VectorL10n.close, style: .plain) { [weak self] in + self?.didValidate?() + } + self.navigationItem.leftBarButtonItem = doneBarButtonItem + } +} + +// MARK: - UIAdaptivePresentationControllerDelegate +extension ReauthFallBackViewController: UIAdaptivePresentationControllerDelegate { + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + self.didCancel?() + } +} diff --git a/Riot/Modules/Reauthentication/ReauthenticationCoordinator.swift b/Riot/Modules/Reauthentication/ReauthenticationCoordinator.swift new file mode 100644 index 000000000..88708d0e4 --- /dev/null +++ b/Riot/Modules/Reauthentication/ReauthenticationCoordinator.swift @@ -0,0 +1,157 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Reauthentication Reauthentication +/* + 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 UIKit + +enum ReauthenticationCoordinatorError: Error { + case failToBuildPasswordParameters +} + +@objcMembers +final class ReauthenticationCoordinator: ReauthenticationCoordinatorType { + + // MARK: - Properties + + // MARK: Private + + private let parameters: ReauthenticationCoordinatorParameters + private let authenticationSessionService: AuthenticationSessionService + private let authenticationParametersBuilder: AuthenticationParametersBuilder + private let authenticatedSessionViewControllerFactory: AuthenticatedSessionViewControllerFactory + + private var ssoAuthenticationPresenter: SSOAuthenticationPresenter? + + private var authenticationSession: SSOAuthentificationSessionProtocol? + + private var presentingViewController: UIViewController { + return self.parameters.presenter.toPresentable() + } + + private weak var passwordViewController: UIViewController? + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: ReauthenticationCoordinatorDelegate? + + // MARK: - Setup + + init(parameters: ReauthenticationCoordinatorParameters) { + self.parameters = parameters + self.authenticationSessionService = AuthenticationSessionService(session: parameters.session) + self.authenticationParametersBuilder = AuthenticationParametersBuilder() + self.authenticatedSessionViewControllerFactory = AuthenticatedSessionViewControllerFactory() + } + + // MARK: - Public methods + + func start() { + + self.authenticationSessionService.authenticationSession(for: self.parameters.authenticationSessionParameters) { (result) in + + switch result { + case .success(let authenticationSessionResult): + + switch authenticationSessionResult { + case .authenticationNotNeeded: + NSLog("[ReauthenticationCoordinator] No need to login again") + self.delegate?.reauthenticationCoordinatorDidComplete(self, withAuthenticationParameters: nil) + case .authenticationNeeded(let authenticationSession): + if self.authenticationSessionService.hasPasswordFlow(inFlows: authenticationSession.flows) { + self.showPasswordAuthentication(with: authenticationSession) + } else if let authenticationFallbackURL = self.authenticationSessionService.firstUncompletedStageAuthenticationFallbackURL(for: authenticationSession) { + + self.showFallbackAuthentication(with: authenticationFallbackURL, authenticationSession: authenticationSession) + } else { + self.delegate?.reauthenticationCoordinator(self, didFailWithError: AuthenticationSessionServiceError.flowNotSupported) + } + } + case .failure(let error): + self.delegate?.reauthenticationCoordinator(self, didFailWithError: error) + } + } + } + + func toPresentable() -> UIViewController { + return self.parameters.presenter.toPresentable() + } + + // MARK: - Private methods + + private func showPasswordAuthentication(with authenticationSession: MXAuthenticationSession) { + guard let userId = parameters.session.myUser.userId else { + return + } + + let passwordViewController = self.authenticatedSessionViewControllerFactory.createPasswordViewController(title: self.parameters.title, message: self.parameters.message) { [weak self] (password) in + + guard let self = self else { + return + } + + guard let sessionId = authenticationSession.session, let authenticationParameters = self.authenticationParametersBuilder.buildPasswordParameters(sessionId: sessionId, userId: userId, password: password) else { + self.delegate?.reauthenticationCoordinator(self, didFailWithError: ReauthenticationCoordinatorError.failToBuildPasswordParameters) + return + } + + self.delegate?.reauthenticationCoordinatorDidComplete(self, withAuthenticationParameters: authenticationParameters) + + } onCancelled: { [weak self] in + guard let self = self else { + return + } + self.delegate?.reauthenticationCoordinatorDidCancel(self) + } + + self.presentingViewController.present(passwordViewController, animated: true) + } + + private func showFallbackAuthentication(with authenticationURL: URL, authenticationSession: MXAuthenticationSession) { + + // NOTE: Prefer use a callback and the same mechanism as SSOAuthentificationSession instead of using custom WKWebView + let reauthFallbackViewController: ReauthFallBackViewController = ReauthFallBackViewController(url: authenticationURL.absoluteString) + reauthFallbackViewController.title = self.parameters.title + + reauthFallbackViewController.didCancel = { [weak self] in + guard let self = self else { + return + } + self.delegate?.reauthenticationCoordinatorDidCancel(self) + } + + reauthFallbackViewController.didValidate = { [weak self] in + guard let self = self else { + return + } + + guard let sessionId = authenticationSession.session else { + self.delegate?.reauthenticationCoordinator(self, didFailWithError: ReauthenticationCoordinatorError.failToBuildPasswordParameters) + return + } + + let authenticationParameters = self.authenticationParametersBuilder.buildOAuthParameters(with: sessionId) + self.delegate?.reauthenticationCoordinatorDidComplete(self, withAuthenticationParameters: authenticationParameters) + } + + let navigationController = RiotNavigationController(rootViewController: reauthFallbackViewController) + + self.presentingViewController.present(navigationController, animated: true) + } +} diff --git a/Riot/Modules/Reauthentication/ReauthenticationCoordinatorBridgePresenter.swift b/Riot/Modules/Reauthentication/ReauthenticationCoordinatorBridgePresenter.swift new file mode 100644 index 000000000..48f62271a --- /dev/null +++ b/Riot/Modules/Reauthentication/ReauthenticationCoordinatorBridgePresenter.swift @@ -0,0 +1,91 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Reauthentication Reauthentication +/* + 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 Foundation + +@objc protocol ReauthenticationCoordinatorBridgePresenterDelegate { + func reauthenticationCoordinatorBridgePresenterDidComplete(_ bridgePresenter: ReauthenticationCoordinatorBridgePresenter, withAuthenticationParameters authenticationParameters: [String: Any]?) + func reauthenticationCoordinatorBridgePresenterDidCancel(_ bridgePresenter: ReauthenticationCoordinatorBridgePresenter) + func reauthenticationCoordinatorBridgePresenter(_ bridgePresenter: ReauthenticationCoordinatorBridgePresenter, didFailWithError error: Error) + +} + +/// ReauthenticationCoordinatorBridgePresenter enables to start ReauthenticationCoordinator from a view controller. +/// This bridge is used while waiting for global usage of coordinator pattern. +/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (mainly for integration in legacy view controllers). +/// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. +@objcMembers +final class ReauthenticationCoordinatorBridgePresenter: NSObject { + + // MARK: - Properties + + // MARK: Private + + private let parameters: ReauthenticationCoordinatorParameters + private var coordinator: ReauthenticationCoordinator? + + // MARK: Public + + weak var delegate: ReauthenticationCoordinatorBridgePresenterDelegate? + + // MARK: - Setup + + init(parameters: ReauthenticationCoordinatorParameters) { + self.parameters = parameters + super.init() + } + + // MARK: - Public + + func present(from viewController: UIViewController, animated: Bool) { + let reauthenticationCoordinator = ReauthenticationCoordinator(parameters: self.parameters) + reauthenticationCoordinator.delegate = self + viewController.present(reauthenticationCoordinator.toPresentable(), animated: animated, completion: nil) + reauthenticationCoordinator.start() + + self.coordinator = reauthenticationCoordinator + } + + func dismiss(animated: Bool, completion: (() -> Void)?) { + guard let coordinator = self.coordinator else { + return + } + coordinator.toPresentable().dismiss(animated: animated) { + self.coordinator = nil + + if let completion = completion { + completion() + } + } + } +} + +// MARK: - ReauthenticationCoordinatorDelegate +extension ReauthenticationCoordinatorBridgePresenter: ReauthenticationCoordinatorDelegate { + func reauthenticationCoordinatorDidComplete(_ coordinator: ReauthenticationCoordinatorType, withAuthenticationParameters authenticationParameters: [String: Any]?) { + self.delegate?.reauthenticationCoordinatorBridgePresenterDidComplete(self, withAuthenticationParameters: authenticationParameters) + } + + func reauthenticationCoordinatorDidCancel(_ coordinator: ReauthenticationCoordinatorType) { + self.delegate?.reauthenticationCoordinatorBridgePresenterDidCancel(self) + } + + func reauthenticationCoordinator(_ coordinator: ReauthenticationCoordinatorType, didFailWithError error: Error) { + self.delegate?.reauthenticationCoordinatorBridgePresenter(self, didFailWithError: error) + } +} diff --git a/Riot/Modules/Reauthentication/ReauthenticationCoordinatorParameters.swift b/Riot/Modules/Reauthentication/ReauthenticationCoordinatorParameters.swift new file mode 100644 index 000000000..fd5ae9577 --- /dev/null +++ b/Riot/Modules/Reauthentication/ReauthenticationCoordinatorParameters.swift @@ -0,0 +1,49 @@ +// +// 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 Foundation + +/// ReauthenticationCoordinator input parameters +@objcMembers +class ReauthenticationCoordinatorParameters: NSObject { + + /// The Matrix session + let session: MXSession + + /// The presenter used to show authentication screen(s) + let presenter: Presentable + + /// The title to use in the authentication screen if present. + let title: String? + + /// The message to use in the authentication screen if present. + let message: String? + + /// The authenticated API endpoint parameters + let authenticationSessionParameters: AuthenticationSessionParameters + + init(session: MXSession, + presenter: Presentable, + title: String?, + message: String?, + authenticationSessionParameters: AuthenticationSessionParameters) { + self.session = session + self.presenter = presenter + self.title = title + self.message = message + self.authenticationSessionParameters = authenticationSessionParameters + } +} diff --git a/Riot/Modules/Reauthentication/ReauthenticationCoordinatorType.swift b/Riot/Modules/Reauthentication/ReauthenticationCoordinatorType.swift new file mode 100644 index 000000000..bdbc31ce2 --- /dev/null +++ b/Riot/Modules/Reauthentication/ReauthenticationCoordinatorType.swift @@ -0,0 +1,30 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Reauthentication Reauthentication +/* + 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 Foundation + +protocol ReauthenticationCoordinatorDelegate: class { + func reauthenticationCoordinatorDidComplete(_ coordinator: ReauthenticationCoordinatorType, withAuthenticationParameters: [String: Any]?) + func reauthenticationCoordinatorDidCancel(_ coordinator: ReauthenticationCoordinatorType) + func reauthenticationCoordinator(_ coordinator: ReauthenticationCoordinatorType, didFailWithError: Error) +} + +/// `ReauthenticationCoordinatorType` is a protocol describing a Coordinator that handle reauthentication. It is used before calling an authenticated API. +protocol ReauthenticationCoordinatorType: Coordinator, Presentable { + var delegate: ReauthenticationCoordinatorDelegate? { get } +} diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 27b14118f..65f156d32 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -121,6 +121,9 @@ #import "EventFormatter.h" #import +#import "SettingsViewController.h" +#import "SecurityViewController.h" + #import "Riot-Swift.h" @interface RoomViewController () UIViewController { return self.secretsResetViewController } + + // MARK: - Private + + private func showAuthentication(with authenticationSessionParameters: AuthenticationSessionParameters) { + + let reauthenticationCoordinatorParameters = ReauthenticationCoordinatorParameters(session: self.session, + presenter: self.toPresentable(), + title: nil, + message: VectorL10n.secretsResetAuthenticationMessage, + authenticationSessionParameters: authenticationSessionParameters) + + let coordinator = ReauthenticationCoordinator(parameters: reauthenticationCoordinatorParameters) + coordinator.delegate = self + coordinator.start() + self.add(childCoordinator: coordinator) + } } // MARK: - SecretsResetViewModelCoordinatorDelegate extension SecretsResetCoordinator: SecretsResetViewModelCoordinatorDelegate { + func secretsResetViewModel(_ viewModel: SecretsResetViewModelType, needsToAuthenticateWith authenticationSessionParamaters: AuthenticationSessionParameters) { + self.showAuthentication(with: authenticationSessionParamaters) + } + func secretsResetViewModelDidResetSecrets(_ viewModel: SecretsResetViewModelType) { self.delegate?.secretsResetCoordinatorDidResetSecrets(self) } @@ -69,3 +89,21 @@ extension SecretsResetCoordinator: SecretsResetViewModelCoordinatorDelegate { self.delegate?.secretsResetCoordinatorDidCancel(self) } } + +// MARK: - ReauthenticationCoordinatorDelegate +extension SecretsResetCoordinator: ReauthenticationCoordinatorDelegate { + + func reauthenticationCoordinatorDidComplete(_ coordinator: ReauthenticationCoordinatorType, withAuthenticationParameters authenticationParameters: [String: Any]?) { + + self.secretsResetViewModel.process(viewAction: .authenticationInfoEntered(authenticationParameters ?? [:])) + } + + func reauthenticationCoordinatorDidCancel(_ coordinator: ReauthenticationCoordinatorType) { + self.remove(childCoordinator: coordinator) + } + + func reauthenticationCoordinator(_ coordinator: ReauthenticationCoordinatorType, didFailWithError error: Error) { + self.secretsResetViewModel.update(viewState: .error(error)) + self.remove(childCoordinator: coordinator) + } +} diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift index 341899aca..6008a1fb0 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift @@ -44,8 +44,6 @@ final class SecretsResetViewController: UIViewController { private var errorPresenter: MXKErrorPresentation! private var activityPresenter: ActivityIndicatorPresenter! - private var authenticatedSessionFactory: AuthenticatedSessionViewControllerFactory? - // MARK: - Setup class func instantiate(with viewModel: SecretsResetViewModelType) -> SecretsResetViewController { @@ -134,8 +132,6 @@ final class SecretsResetViewController: UIViewController { self.renderLoading() case .resetDone: self.renderLoaded() - case .showAuthentication(authData: let authData): - self.showAuthentication(authData: authData) case .error(let error): self.render(error: error) } @@ -153,33 +149,6 @@ final class SecretsResetViewController: UIViewController { self.activityPresenter.removeCurrentActivityIndicator(animated: true) self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) } - - private func showAuthentication(authData: SecretsResetAuthData) { - self.activityPresenter.removeCurrentActivityIndicator(animated: true) - - let authenticatedSessionFactory = authData.authenticatedSessionViewControllerFactory - - authenticatedSessionFactory.viewController(forPath: authData.path, httpMethod: authData.httpMethod, title: nil, message: VectorL10n.secretsResetAuthenticationMessage, onViewController: { [weak self] (viewController) in - guard let self = self else { - return - } - self.present(viewController, animated: true, completion: nil) - }, onAuthenticated: { [weak self] (authInfo) in - guard let self = self else { - return - } - self.viewModel.process(viewAction: .authenticationInfoEntered(authInfo)) - }, onCancelled: { - - }, onFailure: { [weak self] (error) in - guard let self = self else { - return - } - self.render(error: error) - }) - - self.authenticatedSessionFactory = authenticatedSessionFactory - } // MARK: - Actions diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift index 96d59f4da..72330e934 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift @@ -26,6 +26,7 @@ final class SecretsResetViewModel: SecretsResetViewModelType { private let session: MXSession private let recoveryService: MXRecoveryService + private let crossSigningService: CrossSigningService // MARK: Public @@ -37,6 +38,7 @@ final class SecretsResetViewModel: SecretsResetViewModelType { init(session: MXSession) { self.session = session self.recoveryService = session.crypto.recoveryService + self.crossSigningService = CrossSigningService() } // MARK: - Public @@ -47,8 +49,8 @@ final class SecretsResetViewModel: SecretsResetViewModelType { break case .reset: self.askAuthentication() - case .authenticationInfoEntered(let authInfo): - self.resetSecrets(with: authInfo) + case .authenticationInfoEntered(let authParameters): + self.resetSecrets(with: authParameters) case .cancel: self.coordinatorDelegate?.secretsResetViewModelDidCancel(self) } @@ -56,18 +58,18 @@ final class SecretsResetViewModel: SecretsResetViewModelType { // MARK: - Private - private func update(viewState: SecretsResetViewState) { + func update(viewState: SecretsResetViewState) { self.viewDelegate?.secretsResetViewModel(self, didUpdateViewState: viewState) } - private func resetSecrets(with authInfo: [String: Any]) { + private func resetSecrets(with authParameters: [String: Any]) { guard let crossSigning = self.session.crypto.crossSigning else { return } NSLog("[SecretsResetViewModel] resetSecrets") self.update(viewState: .resetting) - crossSigning.setup(withAuthParams: authInfo, success: { [weak self] in + crossSigning.setup(withAuthParams: authParameters, success: { [weak self] in guard let self = self else { return } @@ -93,12 +95,8 @@ final class SecretsResetViewModel: SecretsResetViewModelType { }) } - // NOTE: Use a Coordinator instead of AuthenticatedSessionViewControllerFactory and delegate the presentation to SecretsResetCoordinator private func askAuthentication() { - let path = "\(kMXAPIPrefixPathUnstable)/keys/device_signing/upload" - let authenticatedSessionFactory = AuthenticatedSessionViewControllerFactory(session: self.session) - let authData = SecretsResetAuthData(path: path, httpMethod: "POST", authenticatedSessionViewControllerFactory: authenticatedSessionFactory) - - self.update(viewState: .showAuthentication(authData: authData)) + let authenticationSessionParameters = self.crossSigningService.setupCrossSigningAuthenticationSessionParameters() + self.coordinatorDelegate?.secretsResetViewModel(self, needsToAuthenticateWith: authenticationSessionParameters) } } diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewModelType.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewModelType.swift index 7f40d7238..26b9b295c 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetViewModelType.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewModelType.swift @@ -23,6 +23,7 @@ protocol SecretsResetViewModelViewDelegate: class { } protocol SecretsResetViewModelCoordinatorDelegate: class { + func secretsResetViewModel(_ viewModel: SecretsResetViewModelType, needsToAuthenticateWith authenticationSessionParamaters: AuthenticationSessionParameters) func secretsResetViewModelDidResetSecrets(_ viewModel: SecretsResetViewModelType) func secretsResetViewModelDidCancel(_ viewModel: SecretsResetViewModelType) } @@ -33,5 +34,6 @@ protocol SecretsResetViewModelType { var viewDelegate: SecretsResetViewModelViewDelegate? { get set } var coordinatorDelegate: SecretsResetViewModelCoordinatorDelegate? { get set } + func update(viewState: SecretsResetViewState) func process(viewAction: SecretsResetViewAction) } diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift index d5ce37444..b7cb0acb8 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift @@ -18,15 +18,8 @@ import Foundation -struct SecretsResetAuthData { - let path: String - let httpMethod: String - let authenticatedSessionViewControllerFactory: AuthenticatedSessionViewControllerFactory -} - /// SecretsResetViewController view state enum SecretsResetViewState { - case showAuthentication(authData: SecretsResetAuthData) case resetting case resetDone case error(Error) diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m index fd01afb92..e55696afd 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -139,6 +139,7 @@ TableViewSectionsDelegate> @property (nonatomic, strong) SecureBackupSetupCoordinatorBridgePresenter *secureBackupSetupCoordinatorBridgePresenter; @property (nonatomic, strong) AuthenticatedSessionViewControllerFactory *authenticatedSessionViewControllerFactory; @property (nonatomic, strong) SetPinCoordinatorBridgePresenter *setPinCoordinatorBridgePresenter; +@property (nonatomic, strong) CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter; @end @@ -759,59 +760,42 @@ TableViewSectionsDelegate> - (void)setupCrossSigningWithTitle:(NSString*)title message:(NSString*)message success:(void (^)(void))success - failure:(void (^)(NSError *error))failure + failure:(void (^)(NSError *error))failure + { - __block UIViewController *viewController; [self startActivityIndicator]; - // Get credentials to set up cross-signing - NSString *path = [NSString stringWithFormat:@"%@/keys/device_signing/upload", kMXAPIPrefixPathUnstable]; - _authenticatedSessionViewControllerFactory = [[AuthenticatedSessionViewControllerFactory alloc] initWithSession:self.mainSession]; - [_authenticatedSessionViewControllerFactory viewControllerForPath:path - httpMethod:@"POST" - title:title - message:message - onViewController:^(UIViewController * _Nonnull theViewController) - { - viewController = theViewController; - [self presentViewController:viewController animated:YES completion:nil]; - - } onAuthenticated:^(NSDictionary * _Nonnull authParams) { - - [viewController dismissViewControllerAnimated:NO completion:nil]; - viewController = nil; - - MXCrossSigning *crossSigning = self.mainSession.crypto.crossSigning; - if (crossSigning) - { - [crossSigning setupWithAuthParams:authParams success:^{ - [self stopActivityIndicator]; - [self reloadData]; - success(); - } failure:^(NSError * _Nonnull error) { - [self stopActivityIndicator]; - [self reloadData]; - - [[AppDelegate theDelegate] showErrorAsAlert:error]; - failure(error); - }]; - } - - } onCancelled:^{ - [self stopActivityIndicator]; - - [viewController dismissViewControllerAnimated:NO completion:nil]; - viewController = nil; - failure(nil); - } onFailure:^(NSError * _Nonnull error) { - - [self stopActivityIndicator]; - [[AppDelegate theDelegate] showErrorAsAlert:error]; - - [viewController dismissViewControllerAnimated:NO completion:nil]; - viewController = nil; - failure(error); + MXWeakify(self); + + void (^animationCompletion)(void) = ^void () { + MXStrongifyAndReturnIfNil(self); + + [self stopActivityIndicator]; + [self.crossSigningSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:^{}]; + self.crossSigningSetupCoordinatorBridgePresenter = nil; + }; + + CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter = [[CrossSigningSetupCoordinatorBridgePresenter alloc] initWithSession:self.mainSession]; + + [crossSigningSetupCoordinatorBridgePresenter presentWith:title + message:message + from:self + animated:YES + success:^{ + animationCompletion(); + [self reloadData]; + success(); + } cancel:^{ + animationCompletion(); + failure(nil); + } failure:^(NSError * _Nonnull error) { + animationCompletion(); + [self reloadData]; + [[AppDelegate theDelegate] showErrorAsAlert:error]; + failure(error); }]; + + self.crossSigningSetupCoordinatorBridgePresenter = crossSigningSetupCoordinatorBridgePresenter; } - (void)resetCrossSigning:(id)sender diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 8f6d7c767..a28adeabd 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -249,6 +249,8 @@ TableViewSectionsDelegate> @property (nonatomic, strong) InviteFriendsPresenter *inviteFriendsPresenter; +@property (nonatomic, strong) CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter; + @end @implementation SettingsViewController @@ -4029,58 +4031,42 @@ TableViewSectionsDelegate> message:(NSString*)message success:(void (^)(void))success failure:(void (^)(NSError *error))failure -{ - __block UIViewController *viewController; - [self startActivityIndicator]; - - // Get credentials to set up cross-signing - NSString *path = [NSString stringWithFormat:@"%@/keys/device_signing/upload", kMXAPIPrefixPathUnstable]; - _authenticatedSessionViewControllerFactory = [[AuthenticatedSessionViewControllerFactory alloc] initWithSession:self.mainSession]; - [_authenticatedSessionViewControllerFactory viewControllerForPath:path - httpMethod:@"POST" - title:title - message:message - onViewController:^(UIViewController * _Nonnull theViewController) - { - viewController = theViewController; - [self presentViewController:viewController animated:YES completion:nil]; - - } onAuthenticated:^(NSDictionary * _Nonnull authParams) { - - [viewController dismissViewControllerAnimated:NO completion:nil]; - viewController = nil; - - MXCrossSigning *crossSigning = self.mainSession.crypto.crossSigning; - if (crossSigning) - { - [crossSigning setupWithAuthParams:authParams success:^{ - [self stopActivityIndicator]; - success(); - } failure:^(NSError * _Nonnull error) { - [self stopActivityIndicator]; - - [[AppDelegate theDelegate] showErrorAsAlert:error]; - failure(error); - }]; - } - - } onCancelled:^{ - [self stopActivityIndicator]; - - [viewController dismissViewControllerAnimated:NO completion:nil]; - viewController = nil; - failure(nil); - } onFailure:^(NSError * _Nonnull error) { - - [self stopActivityIndicator]; - [[AppDelegate theDelegate] showErrorAsAlert:error]; - - [viewController dismissViewControllerAnimated:NO completion:nil]; - viewController = nil; - failure(error); - }]; -} +{ + [self startActivityIndicator]; + self.view.userInteractionEnabled = NO; + + MXWeakify(self); + + void (^animationCompletion)(void) = ^void () { + MXStrongifyAndReturnIfNil(self); + + [self stopActivityIndicator]; + self.view.userInteractionEnabled = YES; + [self.crossSigningSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:^{}]; + self.crossSigningSetupCoordinatorBridgePresenter = nil; + }; + + CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter = [[CrossSigningSetupCoordinatorBridgePresenter alloc] initWithSession:self.mainSession]; + + [crossSigningSetupCoordinatorBridgePresenter presentWith:title + message:message + from:self + animated:YES + success:^{ + animationCompletion(); + success(); + } cancel:^{ + animationCompletion(); + failure(nil); + } failure:^(NSError * _Nonnull error) { + animationCompletion(); + [[AppDelegate theDelegate] showErrorAsAlert:error]; + failure(error); + }]; + + self.crossSigningSetupCoordinatorBridgePresenter = crossSigningSetupCoordinatorBridgePresenter; +} #pragma mark - SingleImagePickerPresenterDelegate diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index 7197a42bc..41f61529b 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -26,4 +26,5 @@ #import "RoomSettingsViewController.h" #import "JitsiWidgetData.h" #import "InviteRecentTableViewCell.h" +#import "AuthFallBackViewController.h" #import "CallViewController.h" diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 2fdef058b..b0ac5e6b0 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -126,7 +126,8 @@ class NotificationService: UNNotificationServiceExtension { if !NotificationService.isLoggerInitialized { if isatty(STDERR_FILENO) == 0 { MXLogger.setSubLogName("nse") - MXLogger.redirectNSLog(toFiles: true) + let sizeLimit: UInt = 10 * 1024 * 1024; // 10MB + MXLogger.redirectNSLog(toFiles: true, numberOfFiles: 100, sizeLimit: sizeLimit) } NotificationService.isLoggerInitialized = true } diff --git a/RiotTests/AuthenticationSessionServiceTests.swift b/RiotTests/AuthenticationSessionServiceTests.swift new file mode 100644 index 000000000..f7a89f6d0 --- /dev/null +++ b/RiotTests/AuthenticationSessionServiceTests.swift @@ -0,0 +1,55 @@ +// +// 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 + +@testable import Riot + +class AuthenticationSessionServiceTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + // MARK: - Tests + + func testGetFirstUncompletedStage() { + + let flow1 = MXLoginFlow() + flow1.stages = ["example.type.foo", "example.type.bar"] + + let flow2 = MXLoginFlow() + flow2.stages = ["example.type.foo", "example.type.baz"] + + let completedStages = ["example.type.foo"] + + let authenticationSession = MXAuthenticationSession() + authenticationSession.completed = completedStages + authenticationSession.flows = [flow1, flow2] + + let mxSession = MXSession() + let authenticationSessionService = AuthenticationSessionService(session: mxSession) + + let firstUncompletedStage = authenticationSessionService.firstUncompletedFlowIdentifier(in: authenticationSession) + + XCTAssertNotNil(firstUncompletedStage) + XCTAssertEqual(firstUncompletedStage, "example.type.bar") + } +}