mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Add support for Account Provisioning links. (#4108)
* Add support for account provisioning links and route them to the authentication flow. * Use the provisioning parameters to configure the authentication flow. * Add UI tests for the provisioned authentication flow. * Record new preview snapshots. * Add unit tests. * Make the domain configurable in the app settings. * Use the loginHint in the login screen too.
This commit is contained in:
@@ -55,7 +55,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
|
||||
private let appRouteURLParser: AppRouteURLParser
|
||||
|
||||
@Consumable private var storedAppRoute: AppRoute?
|
||||
private var storedAppRoute: AppRoute?
|
||||
@Consumable private var storedInlineReply: (roomID: String, message: String)?
|
||||
@Consumable private var storedRoomsToAwait: Set<String>?
|
||||
|
||||
@@ -219,6 +219,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
|
||||
if let route = appRouteURLParser.route(from: url) {
|
||||
switch route {
|
||||
case .accountProvisioningLink:
|
||||
handleAppRoute(route)
|
||||
case .genericCallLink(let url):
|
||||
if let userSessionFlowCoordinator {
|
||||
userSessionFlowCoordinator.handleAppRoute(route, animated: true)
|
||||
@@ -508,17 +510,23 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
appSettings: appSettings,
|
||||
appHooks: appHooks)
|
||||
|
||||
authenticationFlowCoordinator = AuthenticationFlowCoordinator(authenticationService: authenticationService,
|
||||
qrCodeLoginService: qrCodeLoginService,
|
||||
bugReportService: ServiceLocator.shared.bugReportService,
|
||||
navigationRootCoordinator: navigationRootCoordinator,
|
||||
appMediator: appMediator,
|
||||
appSettings: appSettings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
authenticationFlowCoordinator?.delegate = self
|
||||
let coordinator = AuthenticationFlowCoordinator(authenticationService: authenticationService,
|
||||
qrCodeLoginService: qrCodeLoginService,
|
||||
bugReportService: ServiceLocator.shared.bugReportService,
|
||||
navigationRootCoordinator: navigationRootCoordinator,
|
||||
appMediator: appMediator,
|
||||
appSettings: appSettings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
coordinator.delegate = self
|
||||
|
||||
authenticationFlowCoordinator?.start()
|
||||
authenticationFlowCoordinator = coordinator
|
||||
coordinator.start()
|
||||
|
||||
if storedAppRoute?.isAuthenticationRoute == true,
|
||||
let storedAppRoute = storedAppRoute.take() {
|
||||
coordinator.handleAppRoute(storedAppRoute, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func runPostSessionSetupTasks() async {
|
||||
@@ -530,7 +538,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
userSession.clientProxy.roomsToAwait = storedRoomsToAwait
|
||||
}
|
||||
|
||||
if let storedAppRoute {
|
||||
if storedAppRoute?.isAuthenticationRoute == false,
|
||||
let storedAppRoute = storedAppRoute.take() {
|
||||
userSessionFlowCoordinator.handleAppRoute(storedAppRoute, animated: false)
|
||||
}
|
||||
|
||||
@@ -796,9 +805,22 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
}
|
||||
|
||||
private func handleAppRoute(_ appRoute: AppRoute) {
|
||||
if let userSessionFlowCoordinator {
|
||||
userSessionFlowCoordinator.handleAppRoute(appRoute, animated: appMediator.appState == .active)
|
||||
} else {
|
||||
var handled = false
|
||||
|
||||
switch appRoute {
|
||||
case .accountProvisioningLink:
|
||||
if let authenticationFlowCoordinator {
|
||||
authenticationFlowCoordinator.handleAppRoute(appRoute, animated: appMediator.appState == .active)
|
||||
handled = true
|
||||
}
|
||||
default:
|
||||
if let userSessionFlowCoordinator {
|
||||
userSessionFlowCoordinator.handleAppRoute(appRoute, animated: appMediator.appState == .active)
|
||||
handled = true
|
||||
}
|
||||
}
|
||||
|
||||
if !handled {
|
||||
storedAppRoute = appRoute
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ final class AppSettings {
|
||||
chatBackupDetailsURL: URL,
|
||||
identityPinningViolationDetailsURL: URL,
|
||||
elementWebHosts: [String],
|
||||
accountProvisioningHost: String,
|
||||
bugReportApplicationID: String,
|
||||
analyticsTermsURL: URL?,
|
||||
mapTilerConfiguration: MapTilerConfiguration) {
|
||||
@@ -121,6 +122,7 @@ final class AppSettings {
|
||||
self.chatBackupDetailsURL = chatBackupDetailsURL
|
||||
self.identityPinningViolationDetailsURL = identityPinningViolationDetailsURL
|
||||
self.elementWebHosts = elementWebHosts
|
||||
self.accountProvisioningHost = accountProvisioningHost
|
||||
self.bugReportApplicationID = bugReportApplicationID
|
||||
self.analyticsTermsURL = analyticsTermsURL
|
||||
self.mapTilerConfiguration = mapTilerConfiguration
|
||||
@@ -176,6 +178,8 @@ final class AppSettings {
|
||||
private(set) var identityPinningViolationDetailsURL: URL = "https://element.io/help#encryption18"
|
||||
/// Any domains that Element web may be hosted on - used for handling links.
|
||||
private(set) var elementWebHosts = ["app.element.io", "staging.element.io", "develop.element.io"]
|
||||
/// The domain that account provisioning links will be hosted on - used for handling the links.
|
||||
private(set) var accountProvisioningHost = "mobile.element.io"
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.appAppearance, defaultValue: .system, storageType: .userDefaults(store))
|
||||
var appAppearance: AppAppearance
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum AppRoute: Equatable, Hashable {
|
||||
// MARK: - Routes
|
||||
|
||||
enum AppRoute: Hashable {
|
||||
/// An account provisioning link generated externally.
|
||||
case accountProvisioningLink(AccountProvisioningParameters)
|
||||
|
||||
/// The app's home screen.
|
||||
case roomList
|
||||
/// A room, shown as the root of the stack (popping any child rooms).
|
||||
@@ -43,6 +48,25 @@ enum AppRoute: Equatable, Hashable {
|
||||
case chatBackupSettings
|
||||
/// An external share request e.g. from the ShareExtension
|
||||
case share(ShareExtensionPayload)
|
||||
|
||||
/// Whether or not the route should be handled by the authentication flow.
|
||||
var isAuthenticationRoute: Bool {
|
||||
switch self {
|
||||
case .accountProvisioningLink: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The parameters parsed out of a provisioning link that can be applied to the authentication flow for a streamlined onboarding experience.
|
||||
struct AccountProvisioningParameters: Hashable {
|
||||
let accountProvider: String
|
||||
let loginHint: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accountProvider = "account_provider"
|
||||
case loginHint = "login_hint"
|
||||
}
|
||||
}
|
||||
|
||||
struct AppRouteURLParser {
|
||||
@@ -53,6 +77,7 @@ struct AppRouteURLParser {
|
||||
AppGroupURLParser(),
|
||||
MatrixPermalinkParser(),
|
||||
ElementWebURLParser(domains: appSettings.elementWebHosts),
|
||||
AccountProvisioningURLParser(domain: appSettings.accountProvisioningHost),
|
||||
ElementCallURLParser()
|
||||
]
|
||||
}
|
||||
@@ -68,6 +93,8 @@ struct AppRouteURLParser {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - URL Parsers
|
||||
|
||||
/// Represents a type that can parse a `URL` into an `AppRoute`.
|
||||
///
|
||||
/// The following Universal Links are missing parsers.
|
||||
@@ -76,7 +103,8 @@ protocol URLParser {
|
||||
func route(from url: URL) -> AppRoute?
|
||||
}
|
||||
|
||||
struct AppGroupURLParser: URLParser {
|
||||
/// The parser for routes that come from app extensions such as the Share Extension.
|
||||
private struct AppGroupURLParser: URLParser {
|
||||
func route(from url: URL) -> AppRoute? {
|
||||
guard let scheme = url.scheme,
|
||||
scheme == InfoPlistReader.app.appScheme,
|
||||
@@ -101,7 +129,7 @@ struct AppGroupURLParser: URLParser {
|
||||
}
|
||||
|
||||
/// The parser for Element Call links. This always returns a `.genericCallLink`.
|
||||
struct ElementCallURLParser: URLParser {
|
||||
private struct ElementCallURLParser: URLParser {
|
||||
private let knownHosts = ["call.element.io"]
|
||||
private let customSchemeURLQueryParameterName = "url"
|
||||
|
||||
@@ -139,7 +167,7 @@ struct ElementCallURLParser: URLParser {
|
||||
}
|
||||
}
|
||||
|
||||
struct MatrixPermalinkParser: URLParser {
|
||||
private struct MatrixPermalinkParser: URLParser {
|
||||
func route(from url: URL) -> AppRoute? {
|
||||
guard let entity = parseMatrixEntityFrom(uri: url.absoluteString) else { return nil }
|
||||
|
||||
@@ -158,7 +186,7 @@ struct MatrixPermalinkParser: URLParser {
|
||||
}
|
||||
}
|
||||
|
||||
struct ElementWebURLParser: URLParser {
|
||||
private struct ElementWebURLParser: URLParser {
|
||||
let domains: [String]
|
||||
let paths = ["room", "user"]
|
||||
|
||||
@@ -187,3 +215,21 @@ struct ElementWebURLParser: URLParser {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
/// The parser for user provisioning links.
|
||||
private struct AccountProvisioningURLParser: URLParser {
|
||||
let domain: String
|
||||
|
||||
func route(from url: URL) -> AppRoute? {
|
||||
guard url.host() == domain else { return nil }
|
||||
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||
let serverName = components.queryItems?.first(where: { $0.name == AccountProvisioningParameters.CodingKeys.accountProvider.rawValue })?.value else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let loginHint = components.queryItems?.first { $0.name == AccountProvisioningParameters.CodingKeys.loginHint.rawValue }?.value
|
||||
|
||||
return .accountProvisioningLink(.init(accountProvider: serverName, loginHint: loginHint))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user