diff --git a/Riot/Modules/Application/LegacyAppDelegate.h b/Riot/Modules/Application/LegacyAppDelegate.h index 459776cf3..6a6d51e4d 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.h +++ b/Riot/Modules/Application/LegacyAppDelegate.h @@ -112,7 +112,7 @@ UINavigationControllerDelegate /** Last handled universal link (url will be formatted for several hash keys). */ -@property (nonatomic, readonly) UniversalLink *lastHandledUniversalLink; +@property (nonatomic, copy, readonly) UniversalLink *lastHandledUniversalLink; // New message sound id. @property (nonatomic, readonly) SystemSoundID messageSound; @@ -162,6 +162,9 @@ UINavigationControllerDelegate // Reload all running matrix sessions - (void)reloadMatrixSessions:(BOOL)clearCache; +- (void)displayLogoutConfirmationForLink:(UniversalLink *)link + completion:(void (^)(BOOL loggedOut))completion; + /** Log out all the accounts after asking for a potential confirmation. Show the authentication screen on successful logout. @@ -252,19 +255,6 @@ UINavigationControllerDelegate */ - (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters; -/** - Extract params from the URL fragment part (after '#') of a vector.im Universal link: - - The fragment can contain a '?'. So there are two kinds of parameters: path params and query params. - It is in the form of /[pathParam1]/[pathParam2]?[queryParam1Key]=[queryParam1Value]&[queryParam2Key]=[queryParam2Value] - @note this method should be private but is used by RoomViewController. This should be moved to a univresal link parser class - - @param fragment the fragment to parse. - @param outPathParams the decoded path params. - @param outQueryParams the decoded query params. If there is no query params, it will be nil. - */ -- (void)parseUniversalLinkFragment:(NSString*)fragment outPathParams:(NSArray **)outPathParams outQueryParams:(NSMutableDictionary **)outQueryParams; - /** Open the dedicated space with the given ID. diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 679da5248..4777ab2e6 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1170,19 +1170,17 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni webURL = [Tools fixURLWithSeveralHashKeys:webURL]; // Extract required parameters from the link - NSArray *pathParams; - NSMutableDictionary *queryParams; - [self parseUniversalLinkFragment:webURL.absoluteString outPathParams:&pathParams outQueryParams:&queryParams]; + UniversalLink *newLink = [[UniversalLink alloc] initWithUrl:webURL]; + NSDictionary *queryParams = newLink.queryParams; - UniversalLink *newLink = [[UniversalLink alloc] initWithUrl:webURL pathParams:pathParams queryParams:queryParams]; if (![_lastHandledUniversalLink isEqual:newLink]) { - _lastHandledUniversalLink = [[UniversalLink alloc] initWithUrl:webURL pathParams:pathParams queryParams:queryParams]; + _lastHandledUniversalLink = newLink; // notify this change [[NSNotificationCenter defaultCenter] postNotificationName:AppDelegateUniversalLinkDidChangeNotification object:nil]; } - if ([self handleServerProvisioningLink:webURL]) + if ([AuthenticationService.shared handleServerProvisioningLink:newLink]) { return YES; } @@ -1281,20 +1279,19 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni return YES; } - return [self handleUniversalLinkFragment:webURL.fragment fromURL:webURL]; + return [self handleUniversalLinkFragment:webURL.fragment fromLink:newLink]; } -- (BOOL)handleUniversalLinkFragment:(NSString*)fragment fromURL:(NSURL*)universalLinkURL - +- (BOOL)handleUniversalLinkFragment:(NSString*)fragment fromLink:(UniversalLink*)universalLink { - if (!fragment || !universalLinkURL) + if (!fragment || !universalLink) { - MXLogDebug(@"[AppDelegate] Cannot handle universal link with missing data: %@ %@", fragment, universalLinkURL); + MXLogDebug(@"[AppDelegate] Cannot handle universal link with missing data: %@ %@", fragment, universalLink.url); return NO; } ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:YES stackAboveVisibleViews:NO]; - UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithFragment:fragment universalLinkURL:universalLinkURL presentationParameters:presentationParameters]; + UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithFragment:fragment universalLink:universalLink presentationParameters:presentationParameters]; return [self handleUniversalLinkWithParameters:parameters]; } @@ -1302,7 +1299,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni - (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)universalLinkParameters { NSString *fragment = universalLinkParameters.fragment; - NSURL *universalLinkURL = universalLinkParameters.universalLinkURL; + UniversalLink *universalLink = universalLinkParameters.universalLink; ScreenPresentationParameters *presentationParameters = universalLinkParameters.presentationParameters; BOOL restoreInitialDisplay = presentationParameters.restoreInitialDisplay; @@ -1320,9 +1317,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self resetPendingUniversalLink]; // Extract params - NSArray *pathParams; - NSMutableDictionary *queryParams; - [self parseUniversalLinkFragment:fragment outPathParams:&pathParams outQueryParams:&queryParams]; + NSArray *pathParams = universalLink.pathParams; + NSDictionary *queryParams = universalLink.queryParams; // Sanity check if (!pathParams.count) @@ -1506,7 +1502,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni self->universalLinkFragmentPendingRoomAlias = @{resolution.roomId: roomIdOrAlias}; UniversalLinkParameters *newParameters = [[UniversalLinkParameters alloc] initWithFragment:newFragment - universalLinkURL:universalLinkURL + universalLink:universalLink presentationParameters:presentationParameters]; [self handleUniversalLinkWithParameters:newParameters]; } @@ -1692,14 +1688,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni }]; } } - // Check whether this is a registration links. - else if ([pathParams[0] isEqualToString:@"register"]) - { - MXLogDebug(@"[AppDelegate] Universal link with registration parameters"); - continueUserActivity = YES; - - [_masterTabBarController showOnboardingFlowWithRegistrationParameters:queryParams]; - } else { // Unknown command: Do nothing except coming back to the main screen @@ -1764,167 +1752,44 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni }]; } -/** - Extract params from the URL fragment part (after '#') of a vector.im Universal link: - - The fragment can contain a '?'. So there are two kinds of parameters: path params and query params. - It is in the form of /[pathParam1]/[pathParam2]?[queryParam1Key]=[queryParam1Value]&[queryParam2Key]=[queryParam2Value] - - @param fragment the fragment to parse. - @param outPathParams the decoded path params. - @param outQueryParams the decoded query params. If there is no query params, it will be nil. - */ -- (void)parseUniversalLinkFragment:(NSString*)fragment outPathParams:(NSArray **)outPathParams outQueryParams:(NSMutableDictionary **)outQueryParams -{ - NSParameterAssert(outPathParams && outQueryParams); - - NSArray *pathParams; - NSMutableDictionary *queryParams; - - NSArray *fragments = [fragment componentsSeparatedByString:@"?"]; - - // Extract path params - pathParams = [fragments[0] componentsSeparatedByString:@"/"]; - - // Remove the first empty path param string - pathParams = [pathParams filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"length > 0"]]; - - // URL decode each path param - NSMutableArray *pathParams2 = [NSMutableArray arrayWithArray:pathParams]; - for (NSInteger i = 0; i < pathParams.count; i++) - { - pathParams2[i] = [pathParams2[i] stringByRemovingPercentEncoding]; - } - pathParams = pathParams2; - - // Extract query params if any - // Query params are in the form [queryParam1Key]=[queryParam1Value], so the - // presence of at least one '=' character is mandatory - if (fragments.count == 2 && (NSNotFound != [fragments[1] rangeOfString:@"="].location)) - { - queryParams = [[NSMutableDictionary alloc] init]; - for (NSString *keyValue in [fragments[1] componentsSeparatedByString:@"&"]) - { - // Get the parameter name - NSString *key = [keyValue componentsSeparatedByString:@"="][0]; - - // Get the parameter value - NSString *value = [keyValue componentsSeparatedByString:@"="][1]; - if (value.length) - { - value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "]; - value = [value stringByRemovingPercentEncoding]; - - if ([key isEqualToString:@"via"]) - { - // Special case the via parameter - // As we can have several of them, store each value into an array - if (!queryParams[key]) - { - queryParams[key] = [NSMutableArray array]; - } - - [queryParams[key] addObject:value]; - } - else - { - queryParams[key] = value; - } - } - } - } - - *outPathParams = pathParams; - *outQueryParams = queryParams; -} - -/** - Parse and handle a server provisioning link. Returns `YES` if a provisioning link was detected and handled. - @param link A link such as https://mobile.element.io/?hs_url=matrix.example.com&is_url=identity.example.com - */ -- (BOOL)handleServerProvisioningLink:(NSURL*)link -{ - MXLogDebug(@"[AppDelegate] handleServerProvisioningLink: %@", link); - - NSString *homeserver, *identityServer; - [self parseServerProvisioningLink:link homeserver:&homeserver identityServer:&identityServer]; - - if (homeserver) - { - if ([MXKAccountManager sharedManager].activeAccounts.count) - { - [self displayServerProvisioningLinkBuyAlreadyLoggedInAlertWithCompletion:^(BOOL logout) { - - MXLogDebug(@"[AppDelegate] handleServerProvisioningLink: logoutWithConfirmation: logout: %@", @(logout)); - if (logout) - { - [self logoutWithConfirmation:NO completion:^(BOOL isLoggedOut) { - [self handleServerProvisioningLink:link]; - }]; - } - }]; - } - else - { - [_masterTabBarController showOnboardingFlow]; - [_masterTabBarController.onboardingCoordinatorBridgePresenter updateHomeserver:homeserver andIdentityServer:identityServer]; - } - - return YES; - } - - return NO; -} - -- (void)parseServerProvisioningLink:(NSURL*)link homeserver:(NSString**)homeserver identityServer:(NSString**)identityServer -{ - if ([link.path isEqualToString:@"/"]) - { - NSURLComponents *linkURLComponents = [NSURLComponents componentsWithURL:link resolvingAgainstBaseURL:NO]; - for (NSURLQueryItem *item in linkURLComponents.queryItems) - { - if ([item.name isEqualToString:@"hs_url"]) - { - *homeserver = item.value; - } - else if ([item.name isEqualToString:@"is_url"]) - { - *identityServer = item.value; - break; - } - } - } - else - { - MXLogDebug(@"[AppDelegate] parseServerProvisioningLink: Error: Unknown path: %@", link.path); - } - - - MXLogDebug(@"[AppDelegate] parseServerProvisioningLink: homeserver: %@ - identityServer: %@", *homeserver, *identityServer); -} - -- (void)displayServerProvisioningLinkBuyAlreadyLoggedInAlertWithCompletion:(void (^)(BOOL logout))completion +- (void)displayLogoutConfirmationForLink:(UniversalLink *)link + completion:(void (^)(BOOL loggedOut))completion { // Ask confirmation - self.logoutConfirmation = [UIAlertController alertControllerWithTitle:[VectorL10n errorUserAlreadyLoggedIn] message:nil preferredStyle:UIAlertControllerStyleAlert]; + self.logoutConfirmation = [UIAlertController alertControllerWithTitle:[VectorL10n errorUserAlreadyLoggedIn] + message:nil + preferredStyle:UIAlertControllerStyleAlert]; [self.logoutConfirmation addAction:[UIAlertAction actionWithTitle:[VectorL10n settingsSignOut] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - self.logoutConfirmation = nil; - completion(YES); - }]]; + self.logoutConfirmation = nil; + [self logoutWithConfirmation:NO completion:^(BOOL isLoggedOut) { + if (isLoggedOut) + { + // process the link again after logging out + [AuthenticationService.shared handleServerProvisioningLink:link]; + } + if (completion) + { + completion(YES); + } + }]; + }]]; [self.logoutConfirmation addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { - self.logoutConfirmation = nil; - completion(NO); - }]]; + self.logoutConfirmation = nil; + if (completion) + { + completion(NO); + } + }]]; - [self.logoutConfirmation mxk_setAccessibilityIdentifier: @"AppDelegateLogoutConfirmationAlert"]; + [self.logoutConfirmation mxk_setAccessibilityIdentifier:@"AppDelegateLogoutConfirmationAlert"]; [self showNotificationAlert:self.logoutConfirmation]; } diff --git a/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift b/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift index 4265c3162..a60b801ba 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift @@ -39,17 +39,10 @@ protocol AuthenticationCoordinatorProtocol: Coordinator, Presentable { /// Update the screen to display registration or login. func update(authenticationFlow: AuthenticationFlow) - - /// Force a registration process based on a predefined set of parameters from a server provisioning link. - /// For more information see `AuthenticationViewController.externalRegistrationParameters`. - func update(externalRegistrationParameters: [AnyHashable: Any]) - + /// Update the screen to use any credentials to use after a soft logout has taken place. func update(softLogoutCredentials: MXCredentials) - - /// Set up the authentication screen with the specified homeserver and/or identity server. - func updateHomeserver(_ homeserver: String?, andIdentityServer identityServer: String?) - + /// Indicates to the coordinator to display any pending screens if it was created with /// the `canPresentAdditionalScreens` parameter set to `false` func presentPendingScreensIfNecessary() diff --git a/Riot/Modules/Authentication/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/LegacyAuthenticationCoordinator.swift index ec614b8b9..a1c74b43f 100644 --- a/Riot/Modules/Authentication/LegacyAuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/LegacyAuthenticationCoordinator.swift @@ -87,18 +87,10 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator authenticationViewController.authType = authenticationFlow.mxkType } - func update(externalRegistrationParameters: [AnyHashable: Any]) { - authenticationViewController.externalRegistrationParameters = externalRegistrationParameters - } - func update(softLogoutCredentials: MXCredentials) { authenticationViewController.softLogoutCredentials = softLogoutCredentials } - - func updateHomeserver(_ homeserver: String?, andIdentityServer identityServer: String?) { - authenticationViewController.showCustomHomeserver(homeserver, andIdentityServer: identityServer) - } - + func presentPendingScreensIfNecessary() { canPresentAdditionalScreens = true @@ -150,6 +142,15 @@ extension LegacyAuthenticationCoordinator: AuthenticationServiceDelegate { func authenticationService(_ service: AuthenticationService, didReceive ssoLoginToken: String, with transactionID: String) -> Bool { authenticationViewController.continueSSOLogin(withToken: ssoLoginToken, txnId: transactionID) } + + func authenticationService(_ service: AuthenticationService, didUpdateStateWithLink link: UniversalLink) { + if link.pathParams.first == "register" && !link.queryParams.isEmpty { + authenticationViewController.externalRegistrationParameters = link.queryParams + } else if let homeserver = link.homeserverUrl { + let identityServer = link.identityServerUrl + authenticationViewController.showCustomHomeserver(homeserver, andIdentityServer: identityServer) + } + } } // MARK: - AuthenticationViewControllerDelegate diff --git a/Riot/Modules/DeepLink/UniversalLinkParameters.swift b/Riot/Modules/DeepLink/UniversalLinkParameters.swift index 6ebe180ed..339ecb82e 100644 --- a/Riot/Modules/DeepLink/UniversalLinkParameters.swift +++ b/Riot/Modules/DeepLink/UniversalLinkParameters.swift @@ -22,8 +22,8 @@ class UniversalLinkParameters: NSObject { // MARK: - Properties - /// The unprocessed universal link URL - let universalLinkURL: URL + /// The universal link + let universalLink: UniversalLink /// The fragment part of the universal link let fragment: String @@ -34,22 +34,33 @@ class UniversalLinkParameters: NSObject { // MARK: - Setup init(fragment: String, - universalLinkURL: URL, + universalLink: UniversalLink, presentationParameters: ScreenPresentationParameters) { self.fragment = fragment - self.universalLinkURL = universalLinkURL + self.universalLink = universalLink self.presentationParameters = presentationParameters super.init() } - convenience init?(universalLinkURL: URL, + convenience init?(universalLink: UniversalLink, presentationParameters: ScreenPresentationParameters) { - guard let fixedURL = Tools.fixURL(withSeveralHashKeys: universalLinkURL), let fragment = fixedURL.fragment else { + guard let fixedURL = Tools.fixURL(withSeveralHashKeys: universalLink.url), let fragment = fixedURL.fragment else { return nil } - self.init(fragment: fragment, universalLinkURL: universalLinkURL, presentationParameters: presentationParameters) + self.init(fragment: fragment, universalLink: universalLink, presentationParameters: presentationParameters) + } + + convenience init?(url: URL, + presentationParameters: ScreenPresentationParameters) { + + guard let fixedURL = Tools.fixURL(withSeveralHashKeys: url), let fragment = fixedURL.fragment else { + return nil + } + let universalLink = UniversalLink(url: fixedURL) + + self.init(fragment: fragment, universalLink: universalLink, presentationParameters: presentationParameters) } } diff --git a/Riot/Modules/Onboarding/AuthenticationCoordinator.swift b/Riot/Modules/Onboarding/AuthenticationCoordinator.swift index fd9319399..91c016b90 100644 --- a/Riot/Modules/Onboarding/AuthenticationCoordinator.swift +++ b/Riot/Modules/Onboarding/AuthenticationCoordinator.swift @@ -17,6 +17,7 @@ */ import UIKit +import CommonKit struct AuthenticationCoordinatorParameters { let navigationRouter: NavigationRouterType @@ -59,6 +60,9 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc /// The listener object that informs the coordinator whether verification needs to be presented or not. private var verificationListener: SessionVerificationListener? + + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var successIndicator: UserIndicator? /// The password entered, for use when setting up cross-signing. private var password: String? @@ -77,6 +81,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc self.navigationRouter = parameters.navigationRouter self.initialScreen = parameters.initialScreen self.canPresentAdditionalScreens = parameters.canPresentAdditionalScreens + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: parameters.navigationRouter.toPresentable()) super.init() } @@ -613,6 +619,15 @@ extension AuthenticationCoordinator: AuthenticationServiceDelegate { return true } + + func authenticationService(_ service: AuthenticationService, didUpdateStateWithLink link: UniversalLink) { + if link.pathParams.first == "register" { + callback?(.cancel(.register)) + } else { + callback?(.cancel(.login)) + } + successIndicator = indicatorPresenter.present(.success(label: VectorL10n.done)) + } } // MARK: - KeyVerificationCoordinatorDelegate @@ -657,17 +672,9 @@ extension AuthenticationCoordinator { // unused } - func update(externalRegistrationParameters: [AnyHashable: Any]) { - // unused - } - func update(softLogoutCredentials: MXCredentials) { // unused } - - func updateHomeserver(_ homeserver: String?, andIdentityServer identityServer: String?) { - // unused - } } // MARK: - AuthFallBackViewControllerDelegate diff --git a/Riot/Modules/Onboarding/OnboardingCoordinator.swift b/Riot/Modules/Onboarding/OnboardingCoordinator.swift index 3521d7ee2..0afc63f64 100644 --- a/Riot/Modules/Onboarding/OnboardingCoordinator.swift +++ b/Riot/Modules/Onboarding/OnboardingCoordinator.swift @@ -42,13 +42,6 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { // MARK: Private private let parameters: OnboardingCoordinatorParameters - // TODO: these can likely be consolidated using an additional authType. - /// The any registration parameters for AuthenticationViewController from a server provisioning link. - private var externalRegistrationParameters: [AnyHashable: Any]? - /// A custom homeserver to be shown when logging in. - private var customHomeserver: String? - /// A custom identity server to be used once logged in. - private var customIdentityServer: String? // MARK: Navigation State private var navigationRouter: NavigationRouterType { @@ -113,21 +106,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { func toPresentable() -> UIViewController { navigationRouter.toPresentable() } - - /// Force a registration process based on a predefined set of parameters from a server provisioning link. - /// For more information see `AuthenticationViewController.externalRegistrationParameters`. - func update(externalRegistrationParameters: [AnyHashable: Any]) { - self.externalRegistrationParameters = externalRegistrationParameters - legacyAuthenticationCoordinator.update(externalRegistrationParameters: externalRegistrationParameters) - } - - /// Set up the authentication screen with the specified homeserver and/or identity server. - func updateHomeserver(_ homeserver: String?, andIdentityServer identityServer: String?) { - self.customHomeserver = homeserver - self.customIdentityServer = identityServer - legacyAuthenticationCoordinator.updateHomeserver(homeserver, andIdentityServer: identityServer) - } - + // MARK: - Pre-Authentication /// Show the onboarding splash screen as the root module in the flow. @@ -259,14 +238,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { break } } - - // Due to needing to preload the authVC, this breaks the Coordinator init/start pattern. - // This can be re-assessed once we re-write a native flow for authentication. - - if let externalRegistrationParameters = externalRegistrationParameters { - coordinator.update(externalRegistrationParameters: externalRegistrationParameters) - } - + coordinator.customServerFieldsVisible = useCaseResult == .customServer if let softLogoutCredentials = parameters.softLogoutCredentials { @@ -277,11 +249,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { coordinator.start() add(childCoordinator: coordinator) - - if customHomeserver != nil || customIdentityServer != nil { - coordinator.updateHomeserver(customHomeserver, andIdentityServer: customIdentityServer) - } - + if navigationRouter.modules.isEmpty { navigationRouter.setRootModule(coordinator, popCompletion: nil) } else { diff --git a/Riot/Modules/Onboarding/OnboardingCoordinatorBridgePresenter.swift b/Riot/Modules/Onboarding/OnboardingCoordinatorBridgePresenter.swift index 8358cd8c1..cdd8c260a 100644 --- a/Riot/Modules/Onboarding/OnboardingCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Onboarding/OnboardingCoordinatorBridgePresenter.swift @@ -20,8 +20,6 @@ import Foundation @objcMembers class OnboardingCoordinatorBridgePresenterParameters: NSObject { - /// The external registration parameters for AuthenticationViewController. - var externalRegistrationParameters: [AnyHashable: Any]? /// The credentials to use after a soft logout has taken place. var softLogoutCredentials: MXCredentials? } @@ -86,17 +84,6 @@ final class OnboardingCoordinatorBridgePresenter: NSObject { self.navigationType = .push } - /// Force a registration process based on a predefined set of parameters from a server provisioning link. - /// For more information see `AuthenticationViewController.externalRegistrationParameters`. - func update(externalRegistrationParameters: [AnyHashable: Any]) { - coordinator?.update(externalRegistrationParameters: externalRegistrationParameters) - } - - /// Set up the authentication screen with the specified homeserver and/or identity server. - func updateHomeserver(_ homeserver: String?, andIdentityServer identityServer: String?) { - coordinator?.updateHomeserver(homeserver, andIdentityServer: identityServer) - } - func dismiss(animated: Bool, completion: (() -> Void)?) { guard let coordinator = self.coordinator else { return @@ -137,10 +124,6 @@ final class OnboardingCoordinatorBridgePresenter: NSObject { onboardingCoordinator.completion = { [weak self] in self?.completion?() } - if let externalRegistrationParameters = parameters.externalRegistrationParameters { - onboardingCoordinator.update(externalRegistrationParameters: externalRegistrationParameters) - } - return onboardingCoordinator } } diff --git a/Riot/Modules/Onboarding/OnboardingCoordinatorProtocol.swift b/Riot/Modules/Onboarding/OnboardingCoordinatorProtocol.swift index 597347bb0..b5147ea30 100644 --- a/Riot/Modules/Onboarding/OnboardingCoordinatorProtocol.swift +++ b/Riot/Modules/Onboarding/OnboardingCoordinatorProtocol.swift @@ -20,11 +20,4 @@ import Foundation /// full onboarding flow with pre-auth screens, authentication and setup screens once signed in. protocol OnboardingCoordinatorProtocol: Coordinator, Presentable { var completion: (() -> Void)? { get set } - - /// Force a registration process based on a predefined set of parameters from a server provisioning link. - /// For more information see `AuthenticationViewController.externalRegistrationParameters`. - func update(externalRegistrationParameters: [AnyHashable: Any]) - - /// Set up the authentication screen with the specified homeserver and/or identity server. - func updateHomeserver(_ homeserver: String?, andIdentityServer identityServer: String?) } diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 814c26f95..96bf22175 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -2375,16 +2375,21 @@ static CGSize kThreadListBarButtonItemImageSize; return [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:BuildSettings.allowSplitViewDetailsScreenStacking sender:self sourceView:nil]; } -- (BOOL)handleUniversalLinkURL:(NSURL*)universalLinkURL +- (BOOL)handleUniversalLinkURL:(NSURL*)url { - UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithUniversalLinkURL:universalLinkURL presentationParameters:[self buildUniversalLinkPresentationParameters]]; + ScreenPresentationParameters *screenParameters = [self buildUniversalLinkPresentationParameters]; + UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithUrl:url + presentationParameters:screenParameters]; return [self handleUniversalLinkWithParameters:parameters]; } -- (BOOL)handleUniversalLinkFragment:(NSString*)fragment fromURL:(NSURL*)universalLinkURL +- (BOOL)handleUniversalLinkFragment:(NSString*)fragment fromURL:(NSURL*)url { + ScreenPresentationParameters *screenParameters = [self buildUniversalLinkPresentationParameters]; + UniversalLink *universalLink = [[UniversalLink alloc] initWithUrl:url]; UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithFragment:fragment - universalLinkURL:universalLinkURL presentationParameters:[self buildUniversalLinkPresentationParameters]]; + universalLink:universalLink + presentationParameters:screenParameters]; return [self handleUniversalLinkWithParameters:parameters]; } diff --git a/Riot/Modules/TabBar/MasterTabBarController.h b/Riot/Modules/TabBar/MasterTabBarController.h index c9948baf9..c6b732f2e 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.h +++ b/Riot/Modules/TabBar/MasterTabBarController.h @@ -63,16 +63,6 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) { */ - (void)showOnboardingFlow; -/** - Display the onboarding flow in order to pursue a registration process by using a predefined set - of parameters. - - If the provided registration parameters are not supported, the default onboarding flow will be used. - - @param parameters the set of parameters. - */ -- (void)showOnboardingFlowWithRegistrationParameters:(NSDictionary*)parameters; - /** Display the onboarding flow configured to log back into a soft logout session. diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 91dab11c1..53e08c86c 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -67,8 +67,6 @@ @property (nonatomic, readwrite) id addAccountObserver; @property (nonatomic, readwrite) id removeAccountObserver; -// The parameters to pass to the Authentication view controller. -@property (nonatomic, readwrite) NSDictionary *authViewControllerRegistrationParameters; @property (nonatomic, readwrite) MXCredentials *softLogoutCredentials; @property (nonatomic) BOOL reviewSessionAlertHasBeenDisplayed; @@ -478,12 +476,6 @@ - (void)presentOnboardingFlow { OnboardingCoordinatorBridgePresenterParameters *parameters = [[OnboardingCoordinatorBridgePresenterParameters alloc] init]; - // Forward parameters if any - if (self.authViewControllerRegistrationParameters) - { - parameters.externalRegistrationParameters = self.authViewControllerRegistrationParameters; - self.authViewControllerRegistrationParameters = nil; - } if (self.softLogoutCredentials) { parameters.softLogoutCredentials = self.softLogoutCredentials; @@ -547,36 +539,6 @@ } } -/** - Sets up authentication with parameters detected in a universal link. For example - https://app.element.io/#/register/?hs_url=matrix.example.com&is_url=identity.example.com - */ - -- (void)showOnboardingFlowWithRegistrationParameters:(NSDictionary *)parameters -{ - if (self.onboardingCoordinatorBridgePresenter) - { - MXLogDebug(@"[MasterTabBarController] Universal link: Forward registration parameter to the existing AuthViewController"); - [self.onboardingCoordinatorBridgePresenter updateWithExternalRegistrationParameters:parameters]; - } - else - { - MXLogDebug(@"[MasterTabBarController] Universal link: Prompt to logout current sessions and open AuthViewController to complete the registration"); - - // Keep a ref on the params - self.authViewControllerRegistrationParameters = parameters; - - // Prompt to logout. It will then display AuthViewController if the user is logged out. - [[AppDelegate theDelegate] logoutWithConfirmation:YES completion:^(BOOL isLoggedOut) { - if (!isLoggedOut) - { - // Reset temporary params - self.authViewControllerRegistrationParameters = nil; - } - }]; - } -} - - (void)showSoftLogoutOnboardingFlowWithCredentials:(MXCredentials*)credentials; { MXLogDebug(@"[MasterTabBarController] showAuthenticationScreenAfterSoftLogout"); diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index 1de9b0dc7..3b4e282b7 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -52,6 +52,7 @@ #import "BubbleRoomTimelineCellProvider.h" #import "RoomSelectedStickerBubbleCell.h" #import "MXRoom+Riot.h" +#import "UniversalLink.h" // MatrixKit common imports, shared with all targets #import "MatrixKit-Bridging-Header.h" diff --git a/Riot/Utils/UniversalLink.h b/Riot/Utils/UniversalLink.h index df546a936..bb7c62952 100644 --- a/Riot/Utils/UniversalLink.h +++ b/Riot/Utils/UniversalLink.h @@ -18,17 +18,27 @@ NS_ASSUME_NONNULL_BEGIN -@interface UniversalLink : NSObject +@interface UniversalLink : NSObject +/// Original url @property (nonatomic, copy, readonly) NSURL *url; +/// Path params from the link. @property (nonatomic, copy, readonly) NSArray *pathParams; -@property (nonatomic, copy, readonly) NSDictionary *queryParams; +/// Query params from the link. Does not conform to RFC 1808. Designed for simplicity. +@property (nonatomic, copy, readonly) NSDictionary *queryParams; -- (id)initWithUrl:(NSURL *)url - pathParams:(NSArray *)pathParams - queryParams:(NSDictionary *)queryParams; +/// Homeserver url in the link if any +@property (nonatomic, copy, readonly, nullable) NSString *homeserverUrl; +/// Identity server url in the link if any +@property (nonatomic, copy, readonly, nullable) NSString *identityServerUrl; +/// via parameters url in the link if any +@property (nonatomic, copy, readonly) NSArray *via; + +/// Initializer +/// @param url original url +- (id)initWithUrl:(NSURL *)url; @end diff --git a/Riot/Utils/UniversalLink.m b/Riot/Utils/UniversalLink.m index 81bb453b4..32ce62dd4 100644 --- a/Riot/Utils/UniversalLink.m +++ b/Riot/Utils/UniversalLink.m @@ -15,21 +15,137 @@ */ #import "UniversalLink.h" +#import "NSArray+Element.h" @implementation UniversalLink -- (id)initWithUrl:(NSURL *)url pathParams:(NSArray *)pathParams queryParams:(NSDictionary *)queryParams +- (id)initWithUrl:(NSURL *)url { self = [super init]; if (self) { _url = url; - _pathParams = pathParams; - _queryParams = queryParams; + + // Extract required parameters from the link + [self parsePathAndQueryParams]; } return self; } +/** + Extract params from the URL fragment part (after '#') of a vector.im Universal link: + + The fragment can contain a '?'. So there are two kinds of parameters: path params and query params. + It is in the form of /[pathParam1]/[pathParam2]?[queryParam1Key]=[queryParam1Value]&[queryParam2Key]=[queryParam2Value] + */ +- (void)parsePathAndQueryParams +{ + NSArray *pathParams; + NSMutableDictionary *queryParams = [NSMutableDictionary dictionary]; + + NSArray *fragments = [_url.fragment componentsSeparatedByString:@"?"]; + + // Extract path params + pathParams = [fragments[0] componentsSeparatedByString:@"/"]; + + // Remove the first empty path param string + pathParams = [pathParams filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"length > 0"]]; + + // URL decode each path param + pathParams = [pathParams vc_map:^id _Nonnull(NSString * _Nonnull item) { + return [item stringByRemovingPercentEncoding]; + }]; + + // Extract query params + NSURLComponents *components = [NSURLComponents componentsWithURL:_url resolvingAgainstBaseURL:NO]; + for (NSURLQueryItem *item in components.queryItems) + { + if (item.value) + { + NSString *key = item.name; + NSString *value = item.value; + value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "]; + value = [value stringByRemovingPercentEncoding]; + + if ([key isEqualToString:@"via"]) + { + // Special case the via parameter + // As we can have several of them, store each value into an array + if (!queryParams[key]) + { + queryParams[key] = [NSMutableArray array]; + } + + [queryParams[key] addObject:value]; + } + else + { + queryParams[key] = value; + } + } + } + // Query params are in the form [queryParam1Key]=[queryParam1Value], so the + // presence of at least one '=' character is mandatory + if (fragments.count == 2 && (NSNotFound != [fragments[1] rangeOfString:@"="].location)) + { + for (NSString *keyValue in [fragments[1] componentsSeparatedByString:@"&"]) + { + // Get the parameter name + NSString *key = [keyValue componentsSeparatedByString:@"="][0]; + + // Get the parameter value + NSString *value = [keyValue componentsSeparatedByString:@"="][1]; + if (value.length) + { + value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "]; + value = [value stringByRemovingPercentEncoding]; + + if ([key isEqualToString:@"via"]) + { + // Special case the via parameter + // As we can have several of them, store each value into an array + if (!queryParams[key]) + { + queryParams[key] = [NSMutableArray array]; + } + + if (![queryParams[key] containsObject:value]) + { + [queryParams[key] addObject:value]; + } + } + else + { + queryParams[key] = value; + } + } + } + } + + _pathParams = pathParams; + _queryParams = queryParams; +} + +- (NSString *)homeserverUrl +{ + return _queryParams[@"hs_url"]; +} + +- (NSString *)identityServerUrl +{ + return _queryParams[@"is_url"]; +} + +- (NSArray *)via +{ + NSArray *result = _queryParams[@"via"]; + if (!result) + { + return @[]; + } + return result; +} + - (BOOL)isEqual:(id)other { if (other == self) @@ -57,4 +173,21 @@ return result; } +- (NSString *)description +{ + return [NSString stringWithFormat:@"", _url.absoluteString]; +} + +#pragma mark - NSCopying +- (id)copyWithZone:(NSZone *)zone +{ + UniversalLink *link = [[self.class allocWithZone:zone] init]; + + link->_url = [_url copyWithZone:zone]; + link->_pathParams = [_pathParams copyWithZone:zone]; + link->_queryParams = [_queryParams copyWithZone:zone]; + + return link; +} + @end diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift index e05c62cc6..9817d59db 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift @@ -25,8 +25,12 @@ protocol AuthenticationServiceDelegate: AnyObject { /// - transactionID: The transaction ID generated during SSO page presentation. /// - Returns: `true` if the SSO login can be continued. func authenticationService(_ service: AuthenticationService, didReceive ssoLoginToken: String, with transactionID: String) -> Bool + + func authenticationService(_ service: AuthenticationService, + didUpdateStateWithLink link: UniversalLink) } +@objcMembers class AuthenticationService: NSObject { /// The shared service object. @@ -73,7 +77,40 @@ class AuthenticationService: NSObject { } // MARK: - Public - + + /// Parse and handle a server provisioning link. + /// - Parameter universalLink: A link such as https://mobile.element.io/?hs_url=matrix.example.com&is_url=identity.example.com + /// - Returns: `true` if a provisioning link was detected and handled. + @discardableResult + func handleServerProvisioningLink(_ universalLink: UniversalLink) -> Bool { + MXLog.debug("[AuthenticationService] handleServerProvisioningLink: \(universalLink)") + + let hsUrl = universalLink.homeserverUrl + let isUrl = universalLink.identityServerUrl + + if hsUrl == nil && isUrl == nil { + MXLog.debug("[AuthenticationService] handleServerProvisioningLink: no hsUrl or isUrl") + return false + } + + let isRegister = universalLink.pathParams.first == "register" + let flow: AuthenticationFlow = isRegister ? .register : .login + + if needsAuthentication { + reset() + // not logged in + // update the state with given HS and IS addresses + state = AuthenticationState(flow: flow, + homeserverAddress: hsUrl ?? BuildSettings.serverConfigDefaultHomeserverUrlString, + identityServer: isUrl ?? BuildSettings.serverConfigDefaultIdentityServerUrlString) + delegate?.authenticationService(self, didUpdateStateWithLink: universalLink) + } else { + // logged in + AppDelegate.theDelegate().displayLogoutConfirmation(for: universalLink, completion: nil) + } + return true + } + /// Whether authentication is needed by checking for any accounts. /// - Returns: `true` there are no accounts or if there is an inactive account that has had a soft logout. var needsAuthentication: Bool { @@ -145,7 +182,10 @@ class AuthenticationService: NSObject { // The previously used homeserver is re-used as `startFlow` will be called again a replace it anyway. let address = state.homeserver.addressFromUser ?? state.homeserver.address - self.state = AuthenticationState(flow: .login, homeserverAddress: address) + let identityServer = state.identityServer + self.state = AuthenticationState(flow: .login, + homeserverAddress: address, + identityServer: identityServer) } /// Continues an SSO flow when completion comes via a deep link. diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift index ccc4f4d97..e8b91a62b 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift @@ -23,16 +23,20 @@ struct AuthenticationState { /// Information about the currently selected homeserver. var homeserver: Homeserver + /// Currently selected identity server + var identityServer: String? var isForceLoginFallbackEnabled = false - init(flow: AuthenticationFlow, homeserverAddress: String) { + init(flow: AuthenticationFlow, homeserverAddress: String, identityServer: String? = nil) { self.flow = flow self.homeserver = Homeserver(address: homeserverAddress) + self.identityServer = identityServer } - init(flow: AuthenticationFlow, homeserver: Homeserver) { + init(flow: AuthenticationFlow, homeserver: Homeserver, identityServer: String? = nil) { self.flow = flow self.homeserver = homeserver + self.identityServer = identityServer } struct Homeserver { diff --git a/RiotTests/UniversalLinkTests.swift b/RiotTests/UniversalLinkTests.swift new file mode 100644 index 000000000..3f856046a --- /dev/null +++ b/RiotTests/UniversalLinkTests.swift @@ -0,0 +1,98 @@ +// +// Copyright 2022 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 UniversalLinkTests: XCTestCase { + + enum UniversalLinkTestError: Error { + case invalidURL + } + + func testInitialization() throws { + guard let url = URL(string: "https://example.com") else { + throw UniversalLinkTestError.invalidURL + } + let universalLink = UniversalLink(url: url) + XCTAssertEqual(universalLink.url, url) + XCTAssertTrue(universalLink.pathParams.isEmpty) + XCTAssertTrue(universalLink.queryParams.isEmpty) + } + + func testRegistrationLink() throws { + guard let url = URL(string: "https://app.element.io/#/register/?hs_url=matrix.example.com&is_url=identity.example.com") else { + throw UniversalLinkTestError.invalidURL + } + let universalLink = UniversalLink(url: url) + XCTAssertEqual(universalLink.url, url) + XCTAssertEqual(universalLink.pathParams.count, 1) + XCTAssertEqual(universalLink.pathParams.first, "register") + XCTAssertEqual(universalLink.queryParams.count, 2) + XCTAssertEqual(universalLink.homeserverUrl, "matrix.example.com") + XCTAssertEqual(universalLink.identityServerUrl, "identity.example.com") + } + + func testLoginLink() throws { + guard let url = URL(string: "https://mobile.element.io/?hs_url=matrix.example.com&is_url=identity.example.com") else { + throw UniversalLinkTestError.invalidURL + } + let universalLink = UniversalLink(url: url) + XCTAssertEqual(universalLink.url, url) + XCTAssertTrue(universalLink.pathParams.isEmpty) + XCTAssertEqual(universalLink.queryParams.count, 2) + XCTAssertEqual(universalLink.homeserverUrl, "matrix.example.com") + XCTAssertEqual(universalLink.identityServerUrl, "identity.example.com") + } + + func testPathParams() throws { + guard let url = URL(string: "https://mobile.element.io/#/param1/param2/param3?hs_url=matrix.example.com&is_url=identity.example.com") else { + throw UniversalLinkTestError.invalidURL + } + let universalLink = UniversalLink(url: url) + XCTAssertEqual(universalLink.url, url) + XCTAssertEqual(universalLink.pathParams.count, 3) + XCTAssertEqual(universalLink.pathParams[0], "param1") + XCTAssertEqual(universalLink.pathParams[1], "param2") + XCTAssertEqual(universalLink.pathParams[2], "param3") + XCTAssertEqual(universalLink.queryParams.count, 2) + XCTAssertEqual(universalLink.homeserverUrl, "matrix.example.com") + XCTAssertEqual(universalLink.identityServerUrl, "identity.example.com") + } + + func testVia() throws { + guard let url = URL(string: "https://mobile.element.io/?hs_url=matrix.example.com&is_url=identity.example.com&via=param1&via=param2") else { + throw UniversalLinkTestError.invalidURL + } + let universalLink = UniversalLink(url: url) + XCTAssertEqual(universalLink.url, url) + XCTAssertEqual(universalLink.queryParams.count, 3) + XCTAssertEqual(universalLink.homeserverUrl, "matrix.example.com") + XCTAssertEqual(universalLink.identityServerUrl, "identity.example.com") + XCTAssertEqual(universalLink.via, ["param1", "param2"]) + } + + func testDescription() throws { + let str = "https://mobile.element.io/?hs_url=matrix.example.com&is_url=identity.example.com&via=param1&via=param2" + guard let url = URL(string: str) else { + throw UniversalLinkTestError.invalidURL + } + let universalLink = UniversalLink(url: url) + let desc = String(format: "", str) + XCTAssertEqual(universalLink.description, desc) + } + +} diff --git a/changelog.d/6180.change b/changelog.d/6180.change new file mode 100644 index 000000000..d98f48c9b --- /dev/null +++ b/changelog.d/6180.change @@ -0,0 +1 @@ +FTUE: Support server provisioning links in the authentication flow.