mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Merge pull request #6250 from vector-im/ismail/6180_support_prov_links
This commit is contained in:
commit
ceae4a455d
19 changed files with 398 additions and 343 deletions
|
@ -112,7 +112,7 @@ UINavigationControllerDelegate
|
||||||
/**
|
/**
|
||||||
Last handled universal link (url will be formatted for several hash keys).
|
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.
|
// New message sound id.
|
||||||
@property (nonatomic, readonly) SystemSoundID messageSound;
|
@property (nonatomic, readonly) SystemSoundID messageSound;
|
||||||
|
@ -162,6 +162,9 @@ UINavigationControllerDelegate
|
||||||
// Reload all running matrix sessions
|
// Reload all running matrix sessions
|
||||||
- (void)reloadMatrixSessions:(BOOL)clearCache;
|
- (void)reloadMatrixSessions:(BOOL)clearCache;
|
||||||
|
|
||||||
|
- (void)displayLogoutConfirmationForLink:(UniversalLink *)link
|
||||||
|
completion:(void (^)(BOOL loggedOut))completion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Log out all the accounts after asking for a potential confirmation.
|
Log out all the accounts after asking for a potential confirmation.
|
||||||
Show the authentication screen on successful logout.
|
Show the authentication screen on successful logout.
|
||||||
|
@ -252,19 +255,6 @@ UINavigationControllerDelegate
|
||||||
*/
|
*/
|
||||||
- (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters;
|
- (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.
|
Open the dedicated space with the given ID.
|
||||||
|
|
||||||
|
|
|
@ -1170,19 +1170,17 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||||
webURL = [Tools fixURLWithSeveralHashKeys:webURL];
|
webURL = [Tools fixURLWithSeveralHashKeys:webURL];
|
||||||
|
|
||||||
// Extract required parameters from the link
|
// Extract required parameters from the link
|
||||||
NSArray<NSString*> *pathParams;
|
UniversalLink *newLink = [[UniversalLink alloc] initWithUrl:webURL];
|
||||||
NSMutableDictionary *queryParams;
|
NSDictionary<NSString*, NSString*> *queryParams = newLink.queryParams;
|
||||||
[self parseUniversalLinkFragment:webURL.absoluteString outPathParams:&pathParams outQueryParams:&queryParams];
|
|
||||||
|
|
||||||
UniversalLink *newLink = [[UniversalLink alloc] initWithUrl:webURL pathParams:pathParams queryParams:queryParams];
|
|
||||||
if (![_lastHandledUniversalLink isEqual:newLink])
|
if (![_lastHandledUniversalLink isEqual:newLink])
|
||||||
{
|
{
|
||||||
_lastHandledUniversalLink = [[UniversalLink alloc] initWithUrl:webURL pathParams:pathParams queryParams:queryParams];
|
_lastHandledUniversalLink = newLink;
|
||||||
// notify this change
|
// notify this change
|
||||||
[[NSNotificationCenter defaultCenter] postNotificationName:AppDelegateUniversalLinkDidChangeNotification object:nil];
|
[[NSNotificationCenter defaultCenter] postNotificationName:AppDelegateUniversalLinkDidChangeNotification object:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([self handleServerProvisioningLink:webURL])
|
if ([AuthenticationService.shared handleServerProvisioningLink:newLink])
|
||||||
{
|
{
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
@ -1281,20 +1279,19 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||||
return YES;
|
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;
|
return NO;
|
||||||
}
|
}
|
||||||
ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:YES stackAboveVisibleViews: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];
|
return [self handleUniversalLinkWithParameters:parameters];
|
||||||
}
|
}
|
||||||
|
@ -1302,7 +1299,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||||
- (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)universalLinkParameters
|
- (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)universalLinkParameters
|
||||||
{
|
{
|
||||||
NSString *fragment = universalLinkParameters.fragment;
|
NSString *fragment = universalLinkParameters.fragment;
|
||||||
NSURL *universalLinkURL = universalLinkParameters.universalLinkURL;
|
UniversalLink *universalLink = universalLinkParameters.universalLink;
|
||||||
ScreenPresentationParameters *presentationParameters = universalLinkParameters.presentationParameters;
|
ScreenPresentationParameters *presentationParameters = universalLinkParameters.presentationParameters;
|
||||||
BOOL restoreInitialDisplay = presentationParameters.restoreInitialDisplay;
|
BOOL restoreInitialDisplay = presentationParameters.restoreInitialDisplay;
|
||||||
|
|
||||||
|
@ -1320,9 +1317,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||||
[self resetPendingUniversalLink];
|
[self resetPendingUniversalLink];
|
||||||
|
|
||||||
// Extract params
|
// Extract params
|
||||||
NSArray<NSString*> *pathParams;
|
NSArray<NSString*> *pathParams = universalLink.pathParams;
|
||||||
NSMutableDictionary *queryParams;
|
NSDictionary<NSString*, NSString*> *queryParams = universalLink.queryParams;
|
||||||
[self parseUniversalLinkFragment:fragment outPathParams:&pathParams outQueryParams:&queryParams];
|
|
||||||
|
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if (!pathParams.count)
|
if (!pathParams.count)
|
||||||
|
@ -1506,7 +1502,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||||
self->universalLinkFragmentPendingRoomAlias = @{resolution.roomId: roomIdOrAlias};
|
self->universalLinkFragmentPendingRoomAlias = @{resolution.roomId: roomIdOrAlias};
|
||||||
|
|
||||||
UniversalLinkParameters *newParameters = [[UniversalLinkParameters alloc] initWithFragment:newFragment
|
UniversalLinkParameters *newParameters = [[UniversalLinkParameters alloc] initWithFragment:newFragment
|
||||||
universalLinkURL:universalLinkURL
|
universalLink:universalLink
|
||||||
presentationParameters:presentationParameters];
|
presentationParameters:presentationParameters];
|
||||||
[self handleUniversalLinkWithParameters:newParameters];
|
[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
|
else
|
||||||
{
|
{
|
||||||
// Unknown command: Do nothing except coming back to the main screen
|
// Unknown command: Do nothing except coming back to the main screen
|
||||||
|
@ -1764,167 +1752,44 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
- (void)displayLogoutConfirmationForLink:(UniversalLink *)link
|
||||||
Extract params from the URL fragment part (after '#') of a vector.im Universal link:
|
completion:(void (^)(BOOL loggedOut))completion
|
||||||
|
|
||||||
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
|
|
||||||
{
|
{
|
||||||
// Ask confirmation
|
// 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]
|
[self.logoutConfirmation addAction:[UIAlertAction actionWithTitle:[VectorL10n settingsSignOut]
|
||||||
style:UIAlertActionStyleDefault
|
style:UIAlertActionStyleDefault
|
||||||
handler:^(UIAlertAction * action)
|
handler:^(UIAlertAction * action)
|
||||||
{
|
{
|
||||||
self.logoutConfirmation = nil;
|
self.logoutConfirmation = nil;
|
||||||
completion(YES);
|
[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]
|
[self.logoutConfirmation addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
|
||||||
style:UIAlertActionStyleCancel
|
style:UIAlertActionStyleCancel
|
||||||
handler:^(UIAlertAction * action)
|
handler:^(UIAlertAction * action)
|
||||||
{
|
{
|
||||||
self.logoutConfirmation = nil;
|
self.logoutConfirmation = nil;
|
||||||
completion(NO);
|
if (completion)
|
||||||
}]];
|
{
|
||||||
|
completion(NO);
|
||||||
|
}
|
||||||
|
}]];
|
||||||
|
|
||||||
[self.logoutConfirmation mxk_setAccessibilityIdentifier: @"AppDelegateLogoutConfirmationAlert"];
|
[self.logoutConfirmation mxk_setAccessibilityIdentifier:@"AppDelegateLogoutConfirmationAlert"];
|
||||||
[self showNotificationAlert:self.logoutConfirmation];
|
[self showNotificationAlert:self.logoutConfirmation];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,17 +39,10 @@ protocol AuthenticationCoordinatorProtocol: Coordinator, Presentable {
|
||||||
|
|
||||||
/// Update the screen to display registration or login.
|
/// Update the screen to display registration or login.
|
||||||
func update(authenticationFlow: AuthenticationFlow)
|
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.
|
/// Update the screen to use any credentials to use after a soft logout has taken place.
|
||||||
func update(softLogoutCredentials: MXCredentials)
|
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
|
/// Indicates to the coordinator to display any pending screens if it was created with
|
||||||
/// the `canPresentAdditionalScreens` parameter set to `false`
|
/// the `canPresentAdditionalScreens` parameter set to `false`
|
||||||
func presentPendingScreensIfNecessary()
|
func presentPendingScreensIfNecessary()
|
||||||
|
|
|
@ -87,18 +87,10 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
|
||||||
authenticationViewController.authType = authenticationFlow.mxkType
|
authenticationViewController.authType = authenticationFlow.mxkType
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(externalRegistrationParameters: [AnyHashable: Any]) {
|
|
||||||
authenticationViewController.externalRegistrationParameters = externalRegistrationParameters
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(softLogoutCredentials: MXCredentials) {
|
func update(softLogoutCredentials: MXCredentials) {
|
||||||
authenticationViewController.softLogoutCredentials = softLogoutCredentials
|
authenticationViewController.softLogoutCredentials = softLogoutCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateHomeserver(_ homeserver: String?, andIdentityServer identityServer: String?) {
|
|
||||||
authenticationViewController.showCustomHomeserver(homeserver, andIdentityServer: identityServer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func presentPendingScreensIfNecessary() {
|
func presentPendingScreensIfNecessary() {
|
||||||
canPresentAdditionalScreens = true
|
canPresentAdditionalScreens = true
|
||||||
|
|
||||||
|
@ -150,6 +142,15 @@ extension LegacyAuthenticationCoordinator: AuthenticationServiceDelegate {
|
||||||
func authenticationService(_ service: AuthenticationService, didReceive ssoLoginToken: String, with transactionID: String) -> Bool {
|
func authenticationService(_ service: AuthenticationService, didReceive ssoLoginToken: String, with transactionID: String) -> Bool {
|
||||||
authenticationViewController.continueSSOLogin(withToken: ssoLoginToken, txnId: transactionID)
|
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
|
// MARK: - AuthenticationViewControllerDelegate
|
||||||
|
|
|
@ -22,8 +22,8 @@ class UniversalLinkParameters: NSObject {
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
||||||
/// The unprocessed universal link URL
|
/// The universal link
|
||||||
let universalLinkURL: URL
|
let universalLink: UniversalLink
|
||||||
|
|
||||||
/// The fragment part of the universal link
|
/// The fragment part of the universal link
|
||||||
let fragment: String
|
let fragment: String
|
||||||
|
@ -34,22 +34,33 @@ class UniversalLinkParameters: NSObject {
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
||||||
init(fragment: String,
|
init(fragment: String,
|
||||||
universalLinkURL: URL,
|
universalLink: UniversalLink,
|
||||||
presentationParameters: ScreenPresentationParameters) {
|
presentationParameters: ScreenPresentationParameters) {
|
||||||
self.fragment = fragment
|
self.fragment = fragment
|
||||||
self.universalLinkURL = universalLinkURL
|
self.universalLink = universalLink
|
||||||
self.presentationParameters = presentationParameters
|
self.presentationParameters = presentationParameters
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init?(universalLinkURL: URL,
|
convenience init?(universalLink: UniversalLink,
|
||||||
presentationParameters: ScreenPresentationParameters) {
|
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
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import CommonKit
|
||||||
|
|
||||||
struct AuthenticationCoordinatorParameters {
|
struct AuthenticationCoordinatorParameters {
|
||||||
let navigationRouter: NavigationRouterType
|
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.
|
/// The listener object that informs the coordinator whether verification needs to be presented or not.
|
||||||
private var verificationListener: SessionVerificationListener?
|
private var verificationListener: SessionVerificationListener?
|
||||||
|
|
||||||
|
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||||
|
private var successIndicator: UserIndicator?
|
||||||
|
|
||||||
/// The password entered, for use when setting up cross-signing.
|
/// The password entered, for use when setting up cross-signing.
|
||||||
private var password: String?
|
private var password: String?
|
||||||
|
@ -77,6 +81,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
||||||
self.navigationRouter = parameters.navigationRouter
|
self.navigationRouter = parameters.navigationRouter
|
||||||
self.initialScreen = parameters.initialScreen
|
self.initialScreen = parameters.initialScreen
|
||||||
self.canPresentAdditionalScreens = parameters.canPresentAdditionalScreens
|
self.canPresentAdditionalScreens = parameters.canPresentAdditionalScreens
|
||||||
|
|
||||||
|
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: parameters.navigationRouter.toPresentable())
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
@ -613,6 +619,15 @@ extension AuthenticationCoordinator: AuthenticationServiceDelegate {
|
||||||
|
|
||||||
return true
|
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
|
// MARK: - KeyVerificationCoordinatorDelegate
|
||||||
|
@ -657,17 +672,9 @@ extension AuthenticationCoordinator {
|
||||||
// unused
|
// unused
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(externalRegistrationParameters: [AnyHashable: Any]) {
|
|
||||||
// unused
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(softLogoutCredentials: MXCredentials) {
|
func update(softLogoutCredentials: MXCredentials) {
|
||||||
// unused
|
// unused
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateHomeserver(_ homeserver: String?, andIdentityServer identityServer: String?) {
|
|
||||||
// unused
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - AuthFallBackViewControllerDelegate
|
// MARK: - AuthFallBackViewControllerDelegate
|
||||||
|
|
|
@ -42,13 +42,6 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private let parameters: OnboardingCoordinatorParameters
|
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
|
// MARK: Navigation State
|
||||||
private var navigationRouter: NavigationRouterType {
|
private var navigationRouter: NavigationRouterType {
|
||||||
|
@ -113,21 +106,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||||
func toPresentable() -> UIViewController {
|
func toPresentable() -> UIViewController {
|
||||||
navigationRouter.toPresentable()
|
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
|
// MARK: - Pre-Authentication
|
||||||
|
|
||||||
/// Show the onboarding splash screen as the root module in the flow.
|
/// Show the onboarding splash screen as the root module in the flow.
|
||||||
|
@ -259,14 +238,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||||
break
|
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
|
coordinator.customServerFieldsVisible = useCaseResult == .customServer
|
||||||
|
|
||||||
if let softLogoutCredentials = parameters.softLogoutCredentials {
|
if let softLogoutCredentials = parameters.softLogoutCredentials {
|
||||||
|
@ -277,11 +249,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||||
|
|
||||||
coordinator.start()
|
coordinator.start()
|
||||||
add(childCoordinator: coordinator)
|
add(childCoordinator: coordinator)
|
||||||
|
|
||||||
if customHomeserver != nil || customIdentityServer != nil {
|
|
||||||
coordinator.updateHomeserver(customHomeserver, andIdentityServer: customIdentityServer)
|
|
||||||
}
|
|
||||||
|
|
||||||
if navigationRouter.modules.isEmpty {
|
if navigationRouter.modules.isEmpty {
|
||||||
navigationRouter.setRootModule(coordinator, popCompletion: nil)
|
navigationRouter.setRootModule(coordinator, popCompletion: nil)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -20,8 +20,6 @@ import Foundation
|
||||||
|
|
||||||
@objcMembers
|
@objcMembers
|
||||||
class OnboardingCoordinatorBridgePresenterParameters: NSObject {
|
class OnboardingCoordinatorBridgePresenterParameters: NSObject {
|
||||||
/// The external registration parameters for AuthenticationViewController.
|
|
||||||
var externalRegistrationParameters: [AnyHashable: Any]?
|
|
||||||
/// The credentials to use after a soft logout has taken place.
|
/// The credentials to use after a soft logout has taken place.
|
||||||
var softLogoutCredentials: MXCredentials?
|
var softLogoutCredentials: MXCredentials?
|
||||||
}
|
}
|
||||||
|
@ -86,17 +84,6 @@ final class OnboardingCoordinatorBridgePresenter: NSObject {
|
||||||
self.navigationType = .push
|
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)?) {
|
func dismiss(animated: Bool, completion: (() -> Void)?) {
|
||||||
guard let coordinator = self.coordinator else {
|
guard let coordinator = self.coordinator else {
|
||||||
return
|
return
|
||||||
|
@ -137,10 +124,6 @@ final class OnboardingCoordinatorBridgePresenter: NSObject {
|
||||||
onboardingCoordinator.completion = { [weak self] in
|
onboardingCoordinator.completion = { [weak self] in
|
||||||
self?.completion?()
|
self?.completion?()
|
||||||
}
|
}
|
||||||
if let externalRegistrationParameters = parameters.externalRegistrationParameters {
|
|
||||||
onboardingCoordinator.update(externalRegistrationParameters: externalRegistrationParameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
return onboardingCoordinator
|
return onboardingCoordinator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,4 @@ import Foundation
|
||||||
/// full onboarding flow with pre-auth screens, authentication and setup screens once signed in.
|
/// full onboarding flow with pre-auth screens, authentication and setup screens once signed in.
|
||||||
protocol OnboardingCoordinatorProtocol: Coordinator, Presentable {
|
protocol OnboardingCoordinatorProtocol: Coordinator, Presentable {
|
||||||
var completion: (() -> Void)? { get set }
|
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?)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2375,16 +2375,21 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||||
return [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:BuildSettings.allowSplitViewDetailsScreenStacking sender:self sourceView:nil];
|
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];
|
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
|
UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithFragment:fragment
|
||||||
universalLinkURL:universalLinkURL presentationParameters:[self buildUniversalLinkPresentationParameters]];
|
universalLink:universalLink
|
||||||
|
presentationParameters:screenParameters];
|
||||||
return [self handleUniversalLinkWithParameters:parameters];
|
return [self handleUniversalLinkWithParameters:parameters];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,16 +63,6 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) {
|
||||||
*/
|
*/
|
||||||
- (void)showOnboardingFlow;
|
- (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.
|
Display the onboarding flow configured to log back into a soft logout session.
|
||||||
|
|
||||||
|
|
|
@ -67,8 +67,6 @@
|
||||||
@property (nonatomic, readwrite) id addAccountObserver;
|
@property (nonatomic, readwrite) id addAccountObserver;
|
||||||
@property (nonatomic, readwrite) id removeAccountObserver;
|
@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, readwrite) MXCredentials *softLogoutCredentials;
|
||||||
|
|
||||||
@property (nonatomic) BOOL reviewSessionAlertHasBeenDisplayed;
|
@property (nonatomic) BOOL reviewSessionAlertHasBeenDisplayed;
|
||||||
|
@ -478,12 +476,6 @@
|
||||||
- (void)presentOnboardingFlow
|
- (void)presentOnboardingFlow
|
||||||
{
|
{
|
||||||
OnboardingCoordinatorBridgePresenterParameters *parameters = [[OnboardingCoordinatorBridgePresenterParameters alloc] init];
|
OnboardingCoordinatorBridgePresenterParameters *parameters = [[OnboardingCoordinatorBridgePresenterParameters alloc] init];
|
||||||
// Forward parameters if any
|
|
||||||
if (self.authViewControllerRegistrationParameters)
|
|
||||||
{
|
|
||||||
parameters.externalRegistrationParameters = self.authViewControllerRegistrationParameters;
|
|
||||||
self.authViewControllerRegistrationParameters = nil;
|
|
||||||
}
|
|
||||||
if (self.softLogoutCredentials)
|
if (self.softLogoutCredentials)
|
||||||
{
|
{
|
||||||
parameters.softLogoutCredentials = 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;
|
- (void)showSoftLogoutOnboardingFlowWithCredentials:(MXCredentials*)credentials;
|
||||||
{
|
{
|
||||||
MXLogDebug(@"[MasterTabBarController] showAuthenticationScreenAfterSoftLogout");
|
MXLogDebug(@"[MasterTabBarController] showAuthenticationScreenAfterSoftLogout");
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
#import "BubbleRoomTimelineCellProvider.h"
|
#import "BubbleRoomTimelineCellProvider.h"
|
||||||
#import "RoomSelectedStickerBubbleCell.h"
|
#import "RoomSelectedStickerBubbleCell.h"
|
||||||
#import "MXRoom+Riot.h"
|
#import "MXRoom+Riot.h"
|
||||||
|
#import "UniversalLink.h"
|
||||||
|
|
||||||
// MatrixKit common imports, shared with all targets
|
// MatrixKit common imports, shared with all targets
|
||||||
#import "MatrixKit-Bridging-Header.h"
|
#import "MatrixKit-Bridging-Header.h"
|
||||||
|
|
|
@ -18,17 +18,27 @@
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@interface UniversalLink : NSObject
|
@interface UniversalLink : NSObject <NSCopying>
|
||||||
|
|
||||||
|
/// Original url
|
||||||
@property (nonatomic, copy, readonly) NSURL *url;
|
@property (nonatomic, copy, readonly) NSURL *url;
|
||||||
|
|
||||||
|
/// Path params from the link.
|
||||||
@property (nonatomic, copy, readonly) NSArray<NSString*> *pathParams;
|
@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
|
/// Homeserver url in the link if any
|
||||||
pathParams:(NSArray<NSString*> *)pathParams
|
@property (nonatomic, copy, readonly, nullable) NSString *homeserverUrl;
|
||||||
queryParams:(NSDictionary<NSString*, NSString*> *)queryParams;
|
/// 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
|
@end
|
||||||
|
|
||||||
|
|
|
@ -15,21 +15,137 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import "UniversalLink.h"
|
#import "UniversalLink.h"
|
||||||
|
#import "NSArray+Element.h"
|
||||||
|
|
||||||
@implementation UniversalLink
|
@implementation UniversalLink
|
||||||
|
|
||||||
- (id)initWithUrl:(NSURL *)url pathParams:(NSArray<NSString *> *)pathParams queryParams:(NSDictionary<NSString *,NSString *> *)queryParams
|
- (id)initWithUrl:(NSURL *)url
|
||||||
{
|
{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
if (self)
|
if (self)
|
||||||
{
|
{
|
||||||
_url = url;
|
_url = url;
|
||||||
_pathParams = pathParams;
|
|
||||||
_queryParams = queryParams;
|
// Extract required parameters from the link
|
||||||
|
[self parsePathAndQueryParams];
|
||||||
}
|
}
|
||||||
return self;
|
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
|
- (BOOL)isEqual:(id)other
|
||||||
{
|
{
|
||||||
if (other == self)
|
if (other == self)
|
||||||
|
@ -57,4 +173,21 @@
|
||||||
return result;
|
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
|
@end
|
||||||
|
|
|
@ -25,8 +25,12 @@ protocol AuthenticationServiceDelegate: AnyObject {
|
||||||
/// - transactionID: The transaction ID generated during SSO page presentation.
|
/// - transactionID: The transaction ID generated during SSO page presentation.
|
||||||
/// - Returns: `true` if the SSO login can be continued.
|
/// - Returns: `true` if the SSO login can be continued.
|
||||||
func authenticationService(_ service: AuthenticationService, didReceive ssoLoginToken: String, with transactionID: String) -> Bool
|
func authenticationService(_ service: AuthenticationService, didReceive ssoLoginToken: String, with transactionID: String) -> Bool
|
||||||
|
|
||||||
|
func authenticationService(_ service: AuthenticationService,
|
||||||
|
didUpdateStateWithLink link: UniversalLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objcMembers
|
||||||
class AuthenticationService: NSObject {
|
class AuthenticationService: NSObject {
|
||||||
|
|
||||||
/// The shared service object.
|
/// The shared service object.
|
||||||
|
@ -73,7 +77,40 @@ class AuthenticationService: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// 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.
|
/// 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.
|
/// - Returns: `true` there are no accounts or if there is an inactive account that has had a soft logout.
|
||||||
var needsAuthentication: Bool {
|
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.
|
// 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
|
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.
|
/// Continues an SSO flow when completion comes via a deep link.
|
||||||
|
|
|
@ -23,16 +23,20 @@ struct AuthenticationState {
|
||||||
|
|
||||||
/// Information about the currently selected homeserver.
|
/// Information about the currently selected homeserver.
|
||||||
var homeserver: Homeserver
|
var homeserver: Homeserver
|
||||||
|
/// Currently selected identity server
|
||||||
|
var identityServer: String?
|
||||||
var isForceLoginFallbackEnabled = false
|
var isForceLoginFallbackEnabled = false
|
||||||
|
|
||||||
init(flow: AuthenticationFlow, homeserverAddress: String) {
|
init(flow: AuthenticationFlow, homeserverAddress: String, identityServer: String? = nil) {
|
||||||
self.flow = flow
|
self.flow = flow
|
||||||
self.homeserver = Homeserver(address: homeserverAddress)
|
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.flow = flow
|
||||||
self.homeserver = homeserver
|
self.homeserver = homeserver
|
||||||
|
self.identityServer = identityServer
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Homeserver {
|
struct Homeserver {
|
||||||
|
|
98
RiotTests/UniversalLinkTests.swift
Normal file
98
RiotTests/UniversalLinkTests.swift
Normal 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
1
changelog.d/6180.change
Normal file
|
@ -0,0 +1 @@
|
||||||
|
FTUE: Support server provisioning links in the authentication flow.
|
Loading…
Reference in a new issue