Merge pull request #6250 from vector-im/ismail/6180_support_prov_links

This commit is contained in:
ismailgulek 2022-06-07 20:54:14 +03:00 committed by GitHub
commit ceae4a455d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 398 additions and 343 deletions

View file

@ -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<NSString*> **)outPathParams outQueryParams:(NSMutableDictionary **)outQueryParams;
/**
Open the dedicated space with the given ID.

View file

@ -1170,19 +1170,17 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
webURL = [Tools fixURLWithSeveralHashKeys:webURL];
// Extract required parameters from the link
NSArray<NSString*> *pathParams;
NSMutableDictionary *queryParams;
[self parseUniversalLinkFragment:webURL.absoluteString outPathParams:&pathParams outQueryParams:&queryParams];
UniversalLink *newLink = [[UniversalLink alloc] initWithUrl:webURL];
NSDictionary<NSString*, NSString*> *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<NSString*> *pathParams;
NSMutableDictionary *queryParams;
[self parseUniversalLinkFragment:fragment outPathParams:&pathParams outQueryParams:&queryParams];
NSArray<NSString*> *pathParams = universalLink.pathParams;
NSDictionary<NSString*, NSString*> *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<NSString*> **)outPathParams outQueryParams:(NSMutableDictionary **)outQueryParams
{
NSParameterAssert(outPathParams && outQueryParams);
NSArray<NSString*> *pathParams;
NSMutableDictionary *queryParams;
NSArray<NSString*> *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<NSString*> *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];
}

View file

@ -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()

View file

@ -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

View file

@ -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)
}
}

View file

@ -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

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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?)
}

View file

@ -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];
}

View file

@ -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.

View file

@ -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");

View file

@ -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"

View file

@ -18,17 +18,27 @@
NS_ASSUME_NONNULL_BEGIN
@interface UniversalLink : NSObject
@interface UniversalLink : NSObject <NSCopying>
/// Original url
@property (nonatomic, copy, readonly) NSURL *url;
/// Path params from the link.
@property (nonatomic, copy, readonly) NSArray<NSString*> *pathParams;
@property (nonatomic, copy, readonly) NSDictionary<NSString*, NSString*> *queryParams;
/// Query params from the link. Does not conform to RFC 1808. Designed for simplicity.
@property (nonatomic, copy, readonly) NSDictionary<NSString*, id> *queryParams;
- (id)initWithUrl:(NSURL *)url
pathParams:(NSArray<NSString*> *)pathParams
queryParams:(NSDictionary<NSString*, NSString*> *)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<NSString*> *via;
/// Initializer
/// @param url original url
- (id)initWithUrl:(NSURL *)url;
@end

View file

@ -15,21 +15,137 @@
*/
#import "UniversalLink.h"
#import "NSArray+Element.h"
@implementation UniversalLink
- (id)initWithUrl:(NSURL *)url pathParams:(NSArray<NSString *> *)pathParams queryParams:(NSDictionary<NSString *,NSString *> *)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<NSString*> *pathParams;
NSMutableDictionary *queryParams = [NSMutableDictionary dictionary];
NSArray<NSString*> *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<NSString *> *)via
{
NSArray<NSString *> *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:@"<UniversalLink: %@>", _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

View file

@ -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.

View file

@ -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 {

View file

@ -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: "<UniversalLink: %@>", str)
XCTAssertEqual(universalLink.description, desc)
}
}

1
changelog.d/6180.change Normal file
View file

@ -0,0 +1 @@
FTUE: Support server provisioning links in the authentication flow.