mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-06 12:28:29 +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:
@@ -608,6 +608,7 @@
|
|||||||
7691233E3572A9173FD96CB3 /* SecureBackupKeyBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */; };
|
7691233E3572A9173FD96CB3 /* SecureBackupKeyBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */; };
|
||||||
76C874243A8C440D6CF7B344 /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; };
|
76C874243A8C440D6CF7B344 /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; };
|
||||||
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; };
|
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; };
|
||||||
|
7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; };
|
||||||
77574A519A4E484880053EAD /* IdentityConfirmationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */; };
|
77574A519A4E484880053EAD /* IdentityConfirmationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */; };
|
||||||
77693820498ABF3508814D49 /* AppLockServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */; };
|
77693820498ABF3508814D49 /* AppLockServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */; };
|
||||||
77920AFA8091AC6B9F190C90 /* Signposter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */; };
|
77920AFA8091AC6B9F190C90 /* Signposter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */; };
|
||||||
@@ -819,7 +820,6 @@
|
|||||||
9E838A62918E47BC72D6640D /* UserIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB54B4F94686CCF0289B72F /* UserIndicatorPresenter.swift */; };
|
9E838A62918E47BC72D6640D /* UserIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB54B4F94686CCF0289B72F /* UserIndicatorPresenter.swift */; };
|
||||||
9EBDC79CAC9B63A0D626E333 /* LegalInformationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EB2CAA266B921D128C35710 /* LegalInformationScreenCoordinator.swift */; };
|
9EBDC79CAC9B63A0D626E333 /* LegalInformationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EB2CAA266B921D128C35710 /* LegalInformationScreenCoordinator.swift */; };
|
||||||
9EE71509E6E7519A2B2388B3 /* KnockRequestsListScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BD9C9A31D9AB3B6D8128E69 /* KnockRequestsListScreenModels.swift */; };
|
9EE71509E6E7519A2B2388B3 /* KnockRequestsListScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BD9C9A31D9AB3B6D8128E69 /* KnockRequestsListScreenModels.swift */; };
|
||||||
9EF72884B74BC39B01640098 /* BugReportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27DF257F5D968E5DD719583C /* BugReportTests.swift */; };
|
|
||||||
9F11B9F347F9E2D236799FB3 /* ElementCallServiceConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */; };
|
9F11B9F347F9E2D236799FB3 /* ElementCallServiceConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */; };
|
||||||
9F11E743EA01482E78A438B0 /* GlobalSearchScreenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22DB19219E6CC4D002E15D48 /* GlobalSearchScreenCell.swift */; };
|
9F11E743EA01482E78A438B0 /* GlobalSearchScreenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22DB19219E6CC4D002E15D48 /* GlobalSearchScreenCell.swift */; };
|
||||||
9FB41B0E8B2AA9B404E52C8B /* AppLockSetupBiometricsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */; };
|
9FB41B0E8B2AA9B404E52C8B /* AppLockSetupBiometricsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */; };
|
||||||
@@ -1577,7 +1577,6 @@
|
|||||||
27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = "<group>"; };
|
27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = "<group>"; };
|
||||||
27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = "<group>"; };
|
27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = "<group>"; };
|
||||||
27D0EA07BD545CC9F234DB8D /* UserDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenModels.swift; sourceTree = "<group>"; };
|
27D0EA07BD545CC9F234DB8D /* UserDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenModels.swift; sourceTree = "<group>"; };
|
||||||
27DF257F5D968E5DD719583C /* BugReportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportTests.swift; sourceTree = "<group>"; };
|
|
||||||
28146817C61423CACCF942F5 /* CallScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenModels.swift; sourceTree = "<group>"; };
|
28146817C61423CACCF942F5 /* CallScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenModels.swift; sourceTree = "<group>"; };
|
||||||
283974987DA7EC61D2AB57D9 /* VoiceMessageCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheTests.swift; sourceTree = "<group>"; };
|
283974987DA7EC61D2AB57D9 /* VoiceMessageCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheTests.swift; sourceTree = "<group>"; };
|
||||||
284FEEB0789B8894E52A7F34 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
284FEEB0789B8894E52A7F34 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
@@ -2343,6 +2342,7 @@
|
|||||||
C618CA2B6C8758B06C88013C /* CreateRoomCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomCoordinator.swift; sourceTree = "<group>"; };
|
C618CA2B6C8758B06C88013C /* CreateRoomCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomCoordinator.swift; sourceTree = "<group>"; };
|
||||||
C687844F60BFF532D49A994C /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = "<group>"; };
|
C687844F60BFF532D49A994C /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = "<group>"; };
|
||||||
C6A9F49B3EE59147AF2F70BB /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = "<group>"; };
|
C6A9F49B3EE59147AF2F70BB /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||||
|
C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportUITests.swift; sourceTree = "<group>"; };
|
||||||
C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = "<group>"; };
|
C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = "<group>"; };
|
||||||
C715CFE00686DACA59D836EA /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/SAS.strings; sourceTree = "<group>"; };
|
C715CFE00686DACA59D836EA /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/SAS.strings; sourceTree = "<group>"; };
|
||||||
C729D95CB4588D4D9AAC3DFA /* RoomChangePermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenModels.swift; sourceTree = "<group>"; };
|
C729D95CB4588D4D9AAC3DFA /* RoomChangePermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenModels.swift; sourceTree = "<group>"; };
|
||||||
@@ -4919,7 +4919,7 @@
|
|||||||
19DD166C3625EE426203FA29 /* AppLockSetupTests.swift */,
|
19DD166C3625EE426203FA29 /* AppLockSetupTests.swift */,
|
||||||
E8495F37D6245AD0CFA1F60B /* AppLockTests.swift */,
|
E8495F37D6245AD0CFA1F60B /* AppLockTests.swift */,
|
||||||
37F46CC4FD89ECF4CF26391A /* AuthenticationFlowCoordinatorTests.swift */,
|
37F46CC4FD89ECF4CF26391A /* AuthenticationFlowCoordinatorTests.swift */,
|
||||||
27DF257F5D968E5DD719583C /* BugReportTests.swift */,
|
C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */,
|
||||||
2214C32EA81FA9168D923D4C /* CreateRoomScreenTests.swift */,
|
2214C32EA81FA9168D923D4C /* CreateRoomScreenTests.swift */,
|
||||||
89BB11A792EF6F70B95B467E /* EncryptionResetTests.swift */,
|
89BB11A792EF6F70B95B467E /* EncryptionResetTests.swift */,
|
||||||
57AD14D3ADADE8F6A10F9E88 /* EncryptionSettingsTests.swift */,
|
57AD14D3ADADE8F6A10F9E88 /* EncryptionSettingsTests.swift */,
|
||||||
@@ -7836,7 +7836,7 @@
|
|||||||
05E797C4E0048BB487E5C4D6 /* AppLockTests.swift in Sources */,
|
05E797C4E0048BB487E5C4D6 /* AppLockTests.swift in Sources */,
|
||||||
7405B4824D45BA7C3D943E76 /* Application.swift in Sources */,
|
7405B4824D45BA7C3D943E76 /* Application.swift in Sources */,
|
||||||
CB07184D37D5D65327A5A693 /* AuthenticationFlowCoordinatorTests.swift in Sources */,
|
CB07184D37D5D65327A5A693 /* AuthenticationFlowCoordinatorTests.swift in Sources */,
|
||||||
9EF72884B74BC39B01640098 /* BugReportTests.swift in Sources */,
|
7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */,
|
||||||
94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */,
|
94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */,
|
||||||
28AB1614E749D1147A2AC6C2 /* CreateRoomScreenTests.swift in Sources */,
|
28AB1614E749D1147A2AC6C2 /* CreateRoomScreenTests.swift in Sources */,
|
||||||
8D24671992A1C1753B211221 /* EncryptionResetTests.swift in Sources */,
|
8D24671992A1C1753B211221 /* EncryptionResetTests.swift in Sources */,
|
||||||
@@ -8657,7 +8657,7 @@
|
|||||||
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
|
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = exactVersion;
|
kind = exactVersion;
|
||||||
version = 25.05.07;
|
version = 25.05.09;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
|
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
|
||||||
|
|||||||
@@ -158,8 +158,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
|
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "143df312f4dd3902d6013aba110d4d9f4093335d",
|
"revision" : "cf913e1417825d08a67cd517b9272b7b808193a8",
|
||||||
"version" : "25.5.7"
|
"version" : "25.5.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
|
|
||||||
private let appRouteURLParser: AppRouteURLParser
|
private let appRouteURLParser: AppRouteURLParser
|
||||||
|
|
||||||
@Consumable private var storedAppRoute: AppRoute?
|
private var storedAppRoute: AppRoute?
|
||||||
@Consumable private var storedInlineReply: (roomID: String, message: String)?
|
@Consumable private var storedInlineReply: (roomID: String, message: String)?
|
||||||
@Consumable private var storedRoomsToAwait: Set<String>?
|
@Consumable private var storedRoomsToAwait: Set<String>?
|
||||||
|
|
||||||
@@ -219,6 +219,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
|
|
||||||
if let route = appRouteURLParser.route(from: url) {
|
if let route = appRouteURLParser.route(from: url) {
|
||||||
switch route {
|
switch route {
|
||||||
|
case .accountProvisioningLink:
|
||||||
|
handleAppRoute(route)
|
||||||
case .genericCallLink(let url):
|
case .genericCallLink(let url):
|
||||||
if let userSessionFlowCoordinator {
|
if let userSessionFlowCoordinator {
|
||||||
userSessionFlowCoordinator.handleAppRoute(route, animated: true)
|
userSessionFlowCoordinator.handleAppRoute(route, animated: true)
|
||||||
@@ -508,17 +510,23 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
appSettings: appSettings,
|
appSettings: appSettings,
|
||||||
appHooks: appHooks)
|
appHooks: appHooks)
|
||||||
|
|
||||||
authenticationFlowCoordinator = AuthenticationFlowCoordinator(authenticationService: authenticationService,
|
let coordinator = AuthenticationFlowCoordinator(authenticationService: authenticationService,
|
||||||
qrCodeLoginService: qrCodeLoginService,
|
qrCodeLoginService: qrCodeLoginService,
|
||||||
bugReportService: ServiceLocator.shared.bugReportService,
|
bugReportService: ServiceLocator.shared.bugReportService,
|
||||||
navigationRootCoordinator: navigationRootCoordinator,
|
navigationRootCoordinator: navigationRootCoordinator,
|
||||||
appMediator: appMediator,
|
appMediator: appMediator,
|
||||||
appSettings: appSettings,
|
appSettings: appSettings,
|
||||||
analytics: ServiceLocator.shared.analytics,
|
analytics: ServiceLocator.shared.analytics,
|
||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
authenticationFlowCoordinator?.delegate = self
|
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 {
|
private func runPostSessionSetupTasks() async {
|
||||||
@@ -530,7 +538,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
userSession.clientProxy.roomsToAwait = storedRoomsToAwait
|
userSession.clientProxy.roomsToAwait = storedRoomsToAwait
|
||||||
}
|
}
|
||||||
|
|
||||||
if let storedAppRoute {
|
if storedAppRoute?.isAuthenticationRoute == false,
|
||||||
|
let storedAppRoute = storedAppRoute.take() {
|
||||||
userSessionFlowCoordinator.handleAppRoute(storedAppRoute, animated: false)
|
userSessionFlowCoordinator.handleAppRoute(storedAppRoute, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,9 +805,22 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handleAppRoute(_ appRoute: AppRoute) {
|
private func handleAppRoute(_ appRoute: AppRoute) {
|
||||||
if let userSessionFlowCoordinator {
|
var handled = false
|
||||||
userSessionFlowCoordinator.handleAppRoute(appRoute, animated: appMediator.appState == .active)
|
|
||||||
} else {
|
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
|
storedAppRoute = appRoute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ final class AppSettings {
|
|||||||
chatBackupDetailsURL: URL,
|
chatBackupDetailsURL: URL,
|
||||||
identityPinningViolationDetailsURL: URL,
|
identityPinningViolationDetailsURL: URL,
|
||||||
elementWebHosts: [String],
|
elementWebHosts: [String],
|
||||||
|
accountProvisioningHost: String,
|
||||||
bugReportApplicationID: String,
|
bugReportApplicationID: String,
|
||||||
analyticsTermsURL: URL?,
|
analyticsTermsURL: URL?,
|
||||||
mapTilerConfiguration: MapTilerConfiguration) {
|
mapTilerConfiguration: MapTilerConfiguration) {
|
||||||
@@ -121,6 +122,7 @@ final class AppSettings {
|
|||||||
self.chatBackupDetailsURL = chatBackupDetailsURL
|
self.chatBackupDetailsURL = chatBackupDetailsURL
|
||||||
self.identityPinningViolationDetailsURL = identityPinningViolationDetailsURL
|
self.identityPinningViolationDetailsURL = identityPinningViolationDetailsURL
|
||||||
self.elementWebHosts = elementWebHosts
|
self.elementWebHosts = elementWebHosts
|
||||||
|
self.accountProvisioningHost = accountProvisioningHost
|
||||||
self.bugReportApplicationID = bugReportApplicationID
|
self.bugReportApplicationID = bugReportApplicationID
|
||||||
self.analyticsTermsURL = analyticsTermsURL
|
self.analyticsTermsURL = analyticsTermsURL
|
||||||
self.mapTilerConfiguration = mapTilerConfiguration
|
self.mapTilerConfiguration = mapTilerConfiguration
|
||||||
@@ -176,6 +178,8 @@ final class AppSettings {
|
|||||||
private(set) var identityPinningViolationDetailsURL: URL = "https://element.io/help#encryption18"
|
private(set) var identityPinningViolationDetailsURL: URL = "https://element.io/help#encryption18"
|
||||||
/// Any domains that Element web may be hosted on - used for handling links.
|
/// 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"]
|
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))
|
@UserPreference(key: UserDefaultsKeys.appAppearance, defaultValue: .system, storageType: .userDefaults(store))
|
||||||
var appAppearance: AppAppearance
|
var appAppearance: AppAppearance
|
||||||
|
|||||||
@@ -8,7 +8,12 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import MatrixRustSDK
|
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.
|
/// The app's home screen.
|
||||||
case roomList
|
case roomList
|
||||||
/// A room, shown as the root of the stack (popping any child rooms).
|
/// A room, shown as the root of the stack (popping any child rooms).
|
||||||
@@ -43,6 +48,25 @@ enum AppRoute: Equatable, Hashable {
|
|||||||
case chatBackupSettings
|
case chatBackupSettings
|
||||||
/// An external share request e.g. from the ShareExtension
|
/// An external share request e.g. from the ShareExtension
|
||||||
case share(ShareExtensionPayload)
|
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 {
|
struct AppRouteURLParser {
|
||||||
@@ -53,6 +77,7 @@ struct AppRouteURLParser {
|
|||||||
AppGroupURLParser(),
|
AppGroupURLParser(),
|
||||||
MatrixPermalinkParser(),
|
MatrixPermalinkParser(),
|
||||||
ElementWebURLParser(domains: appSettings.elementWebHosts),
|
ElementWebURLParser(domains: appSettings.elementWebHosts),
|
||||||
|
AccountProvisioningURLParser(domain: appSettings.accountProvisioningHost),
|
||||||
ElementCallURLParser()
|
ElementCallURLParser()
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -68,6 +93,8 @@ struct AppRouteURLParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - URL Parsers
|
||||||
|
|
||||||
/// Represents a type that can parse a `URL` into an `AppRoute`.
|
/// Represents a type that can parse a `URL` into an `AppRoute`.
|
||||||
///
|
///
|
||||||
/// The following Universal Links are missing parsers.
|
/// The following Universal Links are missing parsers.
|
||||||
@@ -76,7 +103,8 @@ protocol URLParser {
|
|||||||
func route(from url: URL) -> AppRoute?
|
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? {
|
func route(from url: URL) -> AppRoute? {
|
||||||
guard let scheme = url.scheme,
|
guard let scheme = url.scheme,
|
||||||
scheme == InfoPlistReader.app.appScheme,
|
scheme == InfoPlistReader.app.appScheme,
|
||||||
@@ -101,7 +129,7 @@ struct AppGroupURLParser: URLParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The parser for Element Call links. This always returns a `.genericCallLink`.
|
/// 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 knownHosts = ["call.element.io"]
|
||||||
private let customSchemeURLQueryParameterName = "url"
|
private let customSchemeURLQueryParameterName = "url"
|
||||||
|
|
||||||
@@ -139,7 +167,7 @@ struct ElementCallURLParser: URLParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MatrixPermalinkParser: URLParser {
|
private struct MatrixPermalinkParser: URLParser {
|
||||||
func route(from url: URL) -> AppRoute? {
|
func route(from url: URL) -> AppRoute? {
|
||||||
guard let entity = parseMatrixEntityFrom(uri: url.absoluteString) else { return nil }
|
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 domains: [String]
|
||||||
let paths = ["room", "user"]
|
let paths = ["room", "user"]
|
||||||
|
|
||||||
@@ -187,3 +215,21 @@ struct ElementWebURLParser: URLParser {
|
|||||||
return url
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
/// The initial screen shown when you first launch the app.
|
/// The initial screen shown when you first launch the app.
|
||||||
case startScreen
|
case startScreen
|
||||||
|
/// The initial screen with a provisioning link applied.
|
||||||
|
case provisionedStartScreen
|
||||||
|
|
||||||
/// The screen used for the whole QR Code flow.
|
/// The screen used for the whole QR Code flow.
|
||||||
case qrCodeLoginScreen
|
case qrCodeLoginScreen
|
||||||
@@ -55,12 +57,13 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
/// The flow is being started.
|
/// The flow is being started.
|
||||||
case start
|
case start
|
||||||
|
|
||||||
|
/// Modify the flow using the provisioning parameters in the `userInfo`.
|
||||||
|
case applyProvisioningParameters
|
||||||
|
|
||||||
/// The user would like to login with a QR code.
|
/// The user would like to login with a QR code.
|
||||||
case loginWithQR
|
case loginWithQR
|
||||||
/// The user would like to login without a QR code.
|
/// Show the server confirmation screen.
|
||||||
case loginManually
|
case confirmServer(AuthenticationFlow)
|
||||||
/// The user would like to register a new account.
|
|
||||||
case register
|
|
||||||
/// The user encountered a problem.
|
/// The user encountered a problem.
|
||||||
case reportProblem
|
case reportProblem
|
||||||
|
|
||||||
@@ -74,19 +77,19 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
/// The user is no longer selecting a server.
|
/// The user is no longer selecting a server.
|
||||||
case dismissedServerSelection
|
case dismissedServerSelection
|
||||||
|
|
||||||
/// Show the web authentication session for OIDC.
|
/// Show the web authentication session for OIDC (using the parameters in the `userInfo`).
|
||||||
case continueWithOIDC
|
case continueWithOIDC
|
||||||
/// The web authentication session was aborted.
|
/// The web authentication session was aborted.
|
||||||
case cancelledOIDCAuthentication
|
case cancelledOIDCAuthentication(previousState: State)
|
||||||
/// Show the screen to login with password.
|
/// Show the screen to login with password (with the optional login hint in the `userInfo`).
|
||||||
case continueWithPassword
|
case continueWithPassword
|
||||||
/// The password login was aborted.
|
/// The password login was aborted.
|
||||||
case cancelledPasswordLogin
|
case cancelledPasswordLogin(previousState: State)
|
||||||
|
|
||||||
/// The user has finished reporting a problem (or viewing the logs).
|
/// The user has finished reporting a problem (or viewing the logs).
|
||||||
case bugReportFlowComplete
|
case bugReportFlowComplete(previousState: State)
|
||||||
|
|
||||||
/// The user has successfully signed in!
|
/// The user has successfully signed in. The new session can be found in the `userInfo`.
|
||||||
case signedIn
|
case signedIn
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,18 +131,53 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||||
fatalError()
|
switch appRoute {
|
||||||
|
case .accountProvisioningLink(let provisioningParameters):
|
||||||
|
if stateMachine.state != .startScreen {
|
||||||
|
clearRoute(animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMachine.tryEvent(.applyProvisioningParameters, userInfo: provisioningParameters)
|
||||||
|
default:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearRoute(animated: Bool) {
|
func clearRoute(animated: Bool) {
|
||||||
fatalError()
|
switch stateMachine.state {
|
||||||
|
case .initial, .startScreen, .provisionedStartScreen:
|
||||||
|
break
|
||||||
|
case .qrCodeLoginScreen:
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
stateMachine.tryEvent(.cancelledLoginWithQR) // Needs to be handled manually.
|
||||||
|
case .serverConfirmationScreen:
|
||||||
|
navigationStackCoordinator.popToRoot(animated: animated)
|
||||||
|
case .serverSelectionScreen:
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
navigationStackCoordinator.popToRoot(animated: animated)
|
||||||
|
case .oidcAuthentication:
|
||||||
|
oidcPresenter?.cancel()
|
||||||
|
navigationStackCoordinator.popToRoot(animated: animated)
|
||||||
|
case .loginScreen:
|
||||||
|
navigationStackCoordinator.popToRoot(animated: animated)
|
||||||
|
case .bugReportFlow:
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
case .complete:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
||||||
private func configureStateMachine() {
|
private func configureStateMachine() {
|
||||||
stateMachine.addRoutes(event: .start, transitions: [.initial => .startScreen]) { [weak self] _ in
|
stateMachine.addRoutes(event: .start, transitions: [.initial => .startScreen]) { [weak self] _ in
|
||||||
self?.showStartScreen()
|
self?.showStartScreen(fromState: .initial)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMachine.addRoutes(event: .applyProvisioningParameters, transitions: [.initial => .provisionedStartScreen,
|
||||||
|
.startScreen => .provisionedStartScreen]) { [weak self] context in
|
||||||
|
guard let provisioningParameters = context.userInfo as? AccountProvisioningParameters else { fatalError("The authentication configuration is missing.") }
|
||||||
|
self?.showStartScreen(fromState: context.fromState, applying: provisioningParameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
// QR Code
|
// QR Code
|
||||||
@@ -151,10 +189,10 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
// Manual Authentication
|
// Manual Authentication
|
||||||
|
|
||||||
stateMachine.addRoutes(event: .loginManually, transitions: [.startScreen => .serverConfirmationScreen]) { [weak self] _ in
|
stateMachine.addRoutes(event: .confirmServer(.login), transitions: [.startScreen => .serverConfirmationScreen]) { [weak self] _ in
|
||||||
self?.showServerConfirmationScreen(authenticationFlow: .login)
|
self?.showServerConfirmationScreen(authenticationFlow: .login)
|
||||||
}
|
}
|
||||||
stateMachine.addRoutes(event: .register, transitions: [.startScreen => .serverConfirmationScreen]) { [weak self] _ in
|
stateMachine.addRoutes(event: .confirmServer(.register), transitions: [.startScreen => .serverConfirmationScreen]) { [weak self] _ in
|
||||||
self?.showServerConfirmationScreen(authenticationFlow: .register)
|
self?.showServerConfirmationScreen(authenticationFlow: .register)
|
||||||
}
|
}
|
||||||
stateMachine.addRoutes(event: .cancelledServerConfirmation, transitions: [.serverConfirmationScreen => .startScreen])
|
stateMachine.addRoutes(event: .cancelledServerConfirmation, transitions: [.serverConfirmationScreen => .startScreen])
|
||||||
@@ -167,25 +205,32 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
stateMachine.addRoutes(event: .dismissedServerSelection, transitions: [.serverSelectionScreen => .serverConfirmationScreen])
|
stateMachine.addRoutes(event: .dismissedServerSelection, transitions: [.serverSelectionScreen => .serverConfirmationScreen])
|
||||||
|
|
||||||
stateMachine.addRoutes(event: .continueWithOIDC, transitions: [.serverConfirmationScreen => .oidcAuthentication]) { [weak self] context in
|
stateMachine.addRoutes(event: .continueWithOIDC, transitions: [.serverConfirmationScreen => .oidcAuthentication,
|
||||||
|
.provisionedStartScreen => .oidcAuthentication]) { [weak self] context in
|
||||||
guard let (oidcData, window) = context.userInfo as? (OIDCAuthorizationDataProxy, UIWindow) else {
|
guard let (oidcData, window) = context.userInfo as? (OIDCAuthorizationDataProxy, UIWindow) else {
|
||||||
fatalError("Missing the OIDC data and presentation anchor.")
|
fatalError("Missing the OIDC data and presentation anchor.")
|
||||||
}
|
}
|
||||||
self?.showOIDCAuthentication(oidcData: oidcData, presentationAnchor: window)
|
self?.showOIDCAuthentication(oidcData: oidcData, presentationAnchor: window, fromState: context.fromState)
|
||||||
}
|
}
|
||||||
stateMachine.addRoutes(event: .cancelledOIDCAuthentication, transitions: [.oidcAuthentication => .serverConfirmationScreen])
|
stateMachine.addRoutes(event: .cancelledOIDCAuthentication(previousState: .serverConfirmationScreen), transitions: [.oidcAuthentication => .serverConfirmationScreen])
|
||||||
|
stateMachine.addRoutes(event: .cancelledOIDCAuthentication(previousState: .provisionedStartScreen), transitions: [.oidcAuthentication => .provisionedStartScreen])
|
||||||
|
|
||||||
stateMachine.addRoutes(event: .continueWithPassword, transitions: [.serverConfirmationScreen => .loginScreen]) { [weak self] _ in
|
stateMachine.addRoutes(event: .continueWithPassword, transitions: [.serverConfirmationScreen => .loginScreen,
|
||||||
self?.showLoginScreen()
|
.provisionedStartScreen => .loginScreen]) { [weak self] context in
|
||||||
|
let loginHint = context.userInfo as? String
|
||||||
|
self?.showLoginScreen(loginHint: loginHint, fromState: context.fromState)
|
||||||
}
|
}
|
||||||
stateMachine.addRoutes(event: .cancelledPasswordLogin, transitions: [.loginScreen => .serverConfirmationScreen])
|
stateMachine.addRoutes(event: .cancelledPasswordLogin(previousState: .serverConfirmationScreen), transitions: [.loginScreen => .serverConfirmationScreen])
|
||||||
|
stateMachine.addRoutes(event: .cancelledPasswordLogin(previousState: .provisionedStartScreen), transitions: [.loginScreen => .provisionedStartScreen])
|
||||||
|
|
||||||
// Bug Report
|
// Bug Report
|
||||||
|
|
||||||
stateMachine.addRoutes(event: .reportProblem, transitions: [.startScreen => .bugReportFlow]) { [weak self] _ in
|
stateMachine.addRoutes(event: .reportProblem, transitions: [.startScreen => .bugReportFlow,
|
||||||
self?.startBugReportFlow()
|
.provisionedStartScreen => .bugReportFlow]) { [weak self] context in
|
||||||
|
self?.startBugReportFlow(fromState: context.fromState)
|
||||||
}
|
}
|
||||||
stateMachine.addRoutes(event: .bugReportFlowComplete, transitions: [.bugReportFlow => .startScreen])
|
stateMachine.addRoutes(event: .bugReportFlowComplete(previousState: .startScreen), transitions: [.bugReportFlow => .startScreen])
|
||||||
|
stateMachine.addRoutes(event: .bugReportFlowComplete(previousState: .provisionedStartScreen), transitions: [.bugReportFlow => .provisionedStartScreen])
|
||||||
|
|
||||||
// Completion
|
// Completion
|
||||||
|
|
||||||
@@ -208,9 +253,12 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showStartScreen() {
|
private func showStartScreen(fromState: State, applying provisioningParameters: AccountProvisioningParameters? = nil) {
|
||||||
let parameters = AuthenticationStartScreenParameters(showCreateAccountButton: appSettings.showCreateAccountButton,
|
let parameters = AuthenticationStartScreenParameters(authenticationService: authenticationService,
|
||||||
isBugReportServiceEnabled: bugReportService.isEnabled)
|
provisioningParameters: provisioningParameters,
|
||||||
|
isBugReportServiceEnabled: bugReportService.isEnabled,
|
||||||
|
appSettings: appSettings,
|
||||||
|
userIndicatorController: userIndicatorController)
|
||||||
let coordinator = AuthenticationStartScreenCoordinator(parameters: parameters)
|
let coordinator = AuthenticationStartScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
coordinator.actions
|
coordinator.actions
|
||||||
@@ -220,19 +268,26 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
switch action {
|
switch action {
|
||||||
case .loginWithQR:
|
case .loginWithQR:
|
||||||
stateMachine.tryEvent(.loginWithQR)
|
stateMachine.tryEvent(.loginWithQR)
|
||||||
case .loginManually:
|
case .login:
|
||||||
stateMachine.tryEvent(.loginManually)
|
stateMachine.tryEvent(.confirmServer(.login))
|
||||||
case .register:
|
case .register:
|
||||||
stateMachine.tryEvent(.register)
|
stateMachine.tryEvent(.confirmServer(.register))
|
||||||
case .reportProblem:
|
case .reportProblem:
|
||||||
stateMachine.tryEvent(.reportProblem)
|
stateMachine.tryEvent(.reportProblem)
|
||||||
|
|
||||||
|
case .loginDirectlyWithOIDC(let oidcData, let window):
|
||||||
|
stateMachine.tryEvent(.continueWithOIDC, userInfo: (oidcData, window))
|
||||||
|
case .loginDirectlyWithPassword(let loginHint):
|
||||||
|
stateMachine.tryEvent(.continueWithPassword, userInfo: loginHint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
|
|
||||||
navigationRootCoordinator.setRootCoordinator(navigationStackCoordinator)
|
if fromState == .initial {
|
||||||
|
navigationRootCoordinator.setRootCoordinator(navigationStackCoordinator)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - QR Code
|
// MARK: - QR Code
|
||||||
@@ -249,7 +304,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case .signInManually:
|
case .signInManually:
|
||||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
stateMachine.tryEvent(.cancelledLoginWithQR)
|
stateMachine.tryEvent(.cancelledLoginWithQR)
|
||||||
stateMachine.tryEvent(.loginManually)
|
stateMachine.tryEvent(.confirmServer(.login))
|
||||||
case .cancel:
|
case .cancel:
|
||||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
stateMachine.tryEvent(.cancelledLoginWithQR)
|
stateMachine.tryEvent(.cancelledLoginWithQR)
|
||||||
@@ -324,7 +379,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showOIDCAuthentication(oidcData: OIDCAuthorizationDataProxy, presentationAnchor: UIWindow) {
|
private func showOIDCAuthentication(oidcData: OIDCAuthorizationDataProxy, presentationAnchor: UIWindow, fromState: State) {
|
||||||
let presenter = OIDCAuthenticationPresenter(authenticationService: authenticationService,
|
let presenter = OIDCAuthenticationPresenter(authenticationService: authenticationService,
|
||||||
oidcRedirectURL: appSettings.oidcRedirectURL,
|
oidcRedirectURL: appSettings.oidcRedirectURL,
|
||||||
presentationAnchor: presentationAnchor,
|
presentationAnchor: presentationAnchor,
|
||||||
@@ -336,15 +391,16 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case .success(let userSession):
|
case .success(let userSession):
|
||||||
stateMachine.tryEvent(.signedIn, userInfo: userSession)
|
stateMachine.tryEvent(.signedIn, userInfo: userSession)
|
||||||
case .failure:
|
case .failure:
|
||||||
stateMachine.tryEvent(.cancelledOIDCAuthentication)
|
stateMachine.tryEvent(.cancelledOIDCAuthentication(previousState: fromState))
|
||||||
// Nothing more to do, the alerts are handled by the presenter.
|
// Nothing more to do, the alerts are handled by the presenter.
|
||||||
}
|
}
|
||||||
oidcPresenter = nil
|
oidcPresenter = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showLoginScreen() {
|
private func showLoginScreen(loginHint: String?, fromState: State) {
|
||||||
let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService,
|
let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||||
|
loginHint: loginHint,
|
||||||
userIndicatorController: userIndicatorController,
|
userIndicatorController: userIndicatorController,
|
||||||
analytics: analytics)
|
analytics: analytics)
|
||||||
let coordinator = LoginScreenCoordinator(parameters: parameters)
|
let coordinator = LoginScreenCoordinator(parameters: parameters)
|
||||||
@@ -364,13 +420,13 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
navigationStackCoordinator.push(coordinator) { [weak self] in
|
navigationStackCoordinator.push(coordinator) { [weak self] in
|
||||||
self?.stateMachine.tryEvent(.cancelledPasswordLogin)
|
self?.stateMachine.tryEvent(.cancelledPasswordLogin(previousState: fromState))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Bug Report
|
// MARK: - Bug Report
|
||||||
|
|
||||||
private func startBugReportFlow() {
|
private func startBugReportFlow(fromState: State) {
|
||||||
let coordinator = BugReportFlowCoordinator(parameters: .init(presentationMode: .sheet(navigationStackCoordinator),
|
let coordinator = BugReportFlowCoordinator(parameters: .init(presentationMode: .sheet(navigationStackCoordinator),
|
||||||
userIndicatorController: userIndicatorController,
|
userIndicatorController: userIndicatorController,
|
||||||
bugReportService: bugReportService,
|
bugReportService: bugReportService,
|
||||||
@@ -378,7 +434,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
coordinator.actionsPublisher.sink { [weak self] action in
|
coordinator.actionsPublisher.sink { [weak self] action in
|
||||||
switch action {
|
switch action {
|
||||||
case .complete:
|
case .complete:
|
||||||
self?.stateMachine.tryEvent(.bugReportFlowComplete)
|
self?.stateMachine.tryEvent(.bugReportFlowComplete(previousState: fromState))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||||
switch appRoute {
|
switch appRoute {
|
||||||
|
case .accountProvisioningLink:
|
||||||
|
break // We always ignore this flow when logged in.
|
||||||
case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias,
|
case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias,
|
||||||
.roomDetails, .roomMemberDetails, .userProfile,
|
.roomDetails, .roomMemberDetails, .userProfile,
|
||||||
.event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias,
|
.event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias,
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias:
|
case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias:
|
||||||
break // These are converted to a room ID route one level above.
|
break // These are converted to a room ID route one level above.
|
||||||
case .roomList, .userProfile, .call, .genericCallLink, .settings, .chatBackupSettings:
|
case .accountProvisioningLink, .roomList, .userProfile, .call, .genericCallLink, .settings, .chatBackupSettings:
|
||||||
break // These routes can't be handled.
|
break // These routes can't be handled.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,6 +214,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
} else {
|
} else {
|
||||||
stateMachine.processEvent(.showShareExtensionRoomList(sharePayload: payload), userInfo: .init(animated: animated))
|
stateMachine.processEvent(.showShareExtensionRoomList(sharePayload: payload), userInfo: .init(animated: animated))
|
||||||
}
|
}
|
||||||
|
case .accountProvisioningLink:
|
||||||
|
break // We always ignore this flow when logged in.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4273,16 +4273,16 @@ open class ClientSDKMock: MatrixRustSDK.Client, @unchecked Sendable {
|
|||||||
|
|
||||||
//MARK: - urlForOidc
|
//MARK: - urlForOidc
|
||||||
|
|
||||||
open var urlForOidcOidcConfigurationPromptThrowableError: Error?
|
open var urlForOidcOidcConfigurationPromptLoginHintThrowableError: Error?
|
||||||
var urlForOidcOidcConfigurationPromptUnderlyingCallsCount = 0
|
var urlForOidcOidcConfigurationPromptLoginHintUnderlyingCallsCount = 0
|
||||||
open var urlForOidcOidcConfigurationPromptCallsCount: Int {
|
open var urlForOidcOidcConfigurationPromptLoginHintCallsCount: Int {
|
||||||
get {
|
get {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
return urlForOidcOidcConfigurationPromptUnderlyingCallsCount
|
return urlForOidcOidcConfigurationPromptLoginHintUnderlyingCallsCount
|
||||||
} else {
|
} else {
|
||||||
var returnValue: Int? = nil
|
var returnValue: Int? = nil
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
returnValue = urlForOidcOidcConfigurationPromptUnderlyingCallsCount
|
returnValue = urlForOidcOidcConfigurationPromptLoginHintUnderlyingCallsCount
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnValue!
|
return returnValue!
|
||||||
@@ -4290,29 +4290,29 @@ open class ClientSDKMock: MatrixRustSDK.Client, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
urlForOidcOidcConfigurationPromptUnderlyingCallsCount = newValue
|
urlForOidcOidcConfigurationPromptLoginHintUnderlyingCallsCount = newValue
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
urlForOidcOidcConfigurationPromptUnderlyingCallsCount = newValue
|
urlForOidcOidcConfigurationPromptLoginHintUnderlyingCallsCount = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
open var urlForOidcOidcConfigurationPromptCalled: Bool {
|
open var urlForOidcOidcConfigurationPromptLoginHintCalled: Bool {
|
||||||
return urlForOidcOidcConfigurationPromptCallsCount > 0
|
return urlForOidcOidcConfigurationPromptLoginHintCallsCount > 0
|
||||||
}
|
}
|
||||||
open var urlForOidcOidcConfigurationPromptReceivedArguments: (oidcConfiguration: OidcConfiguration, prompt: OidcPrompt?)?
|
open var urlForOidcOidcConfigurationPromptLoginHintReceivedArguments: (oidcConfiguration: OidcConfiguration, prompt: OidcPrompt?, loginHint: String?)?
|
||||||
open var urlForOidcOidcConfigurationPromptReceivedInvocations: [(oidcConfiguration: OidcConfiguration, prompt: OidcPrompt?)] = []
|
open var urlForOidcOidcConfigurationPromptLoginHintReceivedInvocations: [(oidcConfiguration: OidcConfiguration, prompt: OidcPrompt?, loginHint: String?)] = []
|
||||||
|
|
||||||
var urlForOidcOidcConfigurationPromptUnderlyingReturnValue: OAuthAuthorizationData!
|
var urlForOidcOidcConfigurationPromptLoginHintUnderlyingReturnValue: OAuthAuthorizationData!
|
||||||
open var urlForOidcOidcConfigurationPromptReturnValue: OAuthAuthorizationData! {
|
open var urlForOidcOidcConfigurationPromptLoginHintReturnValue: OAuthAuthorizationData! {
|
||||||
get {
|
get {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
return urlForOidcOidcConfigurationPromptUnderlyingReturnValue
|
return urlForOidcOidcConfigurationPromptLoginHintUnderlyingReturnValue
|
||||||
} else {
|
} else {
|
||||||
var returnValue: OAuthAuthorizationData? = nil
|
var returnValue: OAuthAuthorizationData? = nil
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
returnValue = urlForOidcOidcConfigurationPromptUnderlyingReturnValue
|
returnValue = urlForOidcOidcConfigurationPromptLoginHintUnderlyingReturnValue
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnValue!
|
return returnValue!
|
||||||
@@ -4320,29 +4320,29 @@ open class ClientSDKMock: MatrixRustSDK.Client, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
urlForOidcOidcConfigurationPromptUnderlyingReturnValue = newValue
|
urlForOidcOidcConfigurationPromptLoginHintUnderlyingReturnValue = newValue
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
urlForOidcOidcConfigurationPromptUnderlyingReturnValue = newValue
|
urlForOidcOidcConfigurationPromptLoginHintUnderlyingReturnValue = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
open var urlForOidcOidcConfigurationPromptClosure: ((OidcConfiguration, OidcPrompt?) async throws -> OAuthAuthorizationData)?
|
open var urlForOidcOidcConfigurationPromptLoginHintClosure: ((OidcConfiguration, OidcPrompt?, String?) async throws -> OAuthAuthorizationData)?
|
||||||
|
|
||||||
open override func urlForOidc(oidcConfiguration: OidcConfiguration, prompt: OidcPrompt?) async throws -> OAuthAuthorizationData {
|
open override func urlForOidc(oidcConfiguration: OidcConfiguration, prompt: OidcPrompt?, loginHint: String?) async throws -> OAuthAuthorizationData {
|
||||||
if let error = urlForOidcOidcConfigurationPromptThrowableError {
|
if let error = urlForOidcOidcConfigurationPromptLoginHintThrowableError {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
urlForOidcOidcConfigurationPromptCallsCount += 1
|
urlForOidcOidcConfigurationPromptLoginHintCallsCount += 1
|
||||||
urlForOidcOidcConfigurationPromptReceivedArguments = (oidcConfiguration: oidcConfiguration, prompt: prompt)
|
urlForOidcOidcConfigurationPromptLoginHintReceivedArguments = (oidcConfiguration: oidcConfiguration, prompt: prompt, loginHint: loginHint)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.urlForOidcOidcConfigurationPromptReceivedInvocations.append((oidcConfiguration: oidcConfiguration, prompt: prompt))
|
self.urlForOidcOidcConfigurationPromptLoginHintReceivedInvocations.append((oidcConfiguration: oidcConfiguration, prompt: prompt, loginHint: loginHint))
|
||||||
}
|
}
|
||||||
if let urlForOidcOidcConfigurationPromptClosure = urlForOidcOidcConfigurationPromptClosure {
|
if let urlForOidcOidcConfigurationPromptLoginHintClosure = urlForOidcOidcConfigurationPromptLoginHintClosure {
|
||||||
return try await urlForOidcOidcConfigurationPromptClosure(oidcConfiguration, prompt)
|
return try await urlForOidcOidcConfigurationPromptLoginHintClosure(oidcConfiguration, prompt, loginHint)
|
||||||
} else {
|
} else {
|
||||||
return urlForOidcOidcConfigurationPromptReturnValue
|
return urlForOidcOidcConfigurationPromptLoginHintReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10658,6 +10658,71 @@ open class QrCodeDataSDKMock: MatrixRustSDK.QrCodeData, @unchecked Sendable {
|
|||||||
static func reset()
|
static func reset()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//MARK: - serverName
|
||||||
|
|
||||||
|
var serverNameUnderlyingCallsCount = 0
|
||||||
|
open var serverNameCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return serverNameUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = serverNameUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
serverNameUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
serverNameUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var serverNameCalled: Bool {
|
||||||
|
return serverNameCallsCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverNameUnderlyingReturnValue: String?
|
||||||
|
open var serverNameReturnValue: String? {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return serverNameUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: String?? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = serverNameUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
serverNameUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
serverNameUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var serverNameClosure: (() -> String?)?
|
||||||
|
|
||||||
|
open override func serverName() -> String? {
|
||||||
|
serverNameCallsCount += 1
|
||||||
|
if let serverNameClosure = serverNameClosure {
|
||||||
|
return serverNameClosure()
|
||||||
|
} else {
|
||||||
|
return serverNameReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
open class RoomSDKMock: MatrixRustSDK.Room, @unchecked Sendable {
|
open class RoomSDKMock: MatrixRustSDK.Room, @unchecked Sendable {
|
||||||
init() {
|
init() {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ extension ClientSDKMock {
|
|||||||
slidingSyncVersionReturnValue = configuration.slidingSyncVersion
|
slidingSyncVersionReturnValue = configuration.slidingSyncVersion
|
||||||
userIdServerNameThrowableError = MockError.generic
|
userIdServerNameThrowableError = MockError.generic
|
||||||
serverReturnValue = "https://\(configuration.serverAddress)"
|
serverReturnValue = "https://\(configuration.serverAddress)"
|
||||||
urlForOidcOidcConfigurationPromptReturnValue = OAuthAuthorizationDataSDKMock(configuration: configuration)
|
urlForOidcOidcConfigurationPromptLoginHintReturnValue = OAuthAuthorizationDataSDKMock(configuration: configuration)
|
||||||
loginUsernamePasswordInitialDeviceNameDeviceIdClosure = { username, password, _, _ in
|
loginUsernamePasswordInitialDeviceNameDeviceIdClosure = { username, password, _, _ in
|
||||||
guard username == configuration.validCredentials.username,
|
guard username == configuration.validCredentials.username,
|
||||||
password == configuration.validCredentials.password else {
|
password == configuration.validCredentials.password else {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ enum A11yIdentifiers {
|
|||||||
let screenshot = "bug_report-screenshot"
|
let screenshot = "bug_report-screenshot"
|
||||||
let removeScreenshot = "bug_report-remove_screenshot"
|
let removeScreenshot = "bug_report-remove_screenshot"
|
||||||
let attachScreenshot = "bug-report-attach_screenshot"
|
let attachScreenshot = "bug-report-attach_screenshot"
|
||||||
|
let cancel = "bug_report-cancel"
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChangeServer {
|
struct ChangeServer {
|
||||||
@@ -121,6 +122,7 @@ enum A11yIdentifiers {
|
|||||||
struct AuthenticationStartScreen {
|
struct AuthenticationStartScreen {
|
||||||
let signIn = "authentication_start-sign_in"
|
let signIn = "authentication_start-sign_in"
|
||||||
let signInWithQr = "authentication_start-sign_in_with_qr"
|
let signInWithQr = "authentication_start-sign_in_with_qr"
|
||||||
|
let reportAProblem = "authentication_start-report_a_problem"
|
||||||
let hidden = "authentication_start-hidden"
|
let hidden = "authentication_start-hidden"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import SwiftUI
|
|||||||
struct LoginScreenCoordinatorParameters {
|
struct LoginScreenCoordinatorParameters {
|
||||||
/// The service used to authenticate the user.
|
/// The service used to authenticate the user.
|
||||||
let authenticationService: AuthenticationServiceProtocol
|
let authenticationService: AuthenticationServiceProtocol
|
||||||
|
/// An optional hint that can be used to pre-fill the form.
|
||||||
|
let loginHint: String?
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
let analytics: AnalyticsService
|
let analytics: AnalyticsService
|
||||||
}
|
}
|
||||||
@@ -42,6 +44,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
|||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
viewModel = LoginScreenViewModel(authenticationService: parameters.authenticationService,
|
viewModel = LoginScreenViewModel(authenticationService: parameters.authenticationService,
|
||||||
|
loginHint: parameters.loginHint,
|
||||||
userIndicatorController: parameters.userIndicatorController,
|
userIndicatorController: parameters.userIndicatorController,
|
||||||
analytics: parameters.analytics)
|
analytics: parameters.analytics)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,21 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(authenticationService: AuthenticationServiceProtocol,
|
init(authenticationService: AuthenticationServiceProtocol,
|
||||||
|
loginHint: String?,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol,
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
analytics: AnalyticsService) {
|
analytics: AnalyticsService) {
|
||||||
self.authenticationService = authenticationService
|
self.authenticationService = authenticationService
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
self.analytics = analytics
|
self.analytics = analytics
|
||||||
|
|
||||||
let viewState = LoginScreenViewState(homeserver: authenticationService.homeserver.value)
|
let username = switch loginHint {
|
||||||
|
case .some(let hint) where hint.hasPrefix("mxid:"): String(hint.dropFirst(5)) // MSC4198
|
||||||
|
case .some(let hint): hint
|
||||||
|
case .none: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewState = LoginScreenViewState(homeserver: authenticationService.homeserver.value,
|
||||||
|
bindings: LoginScreenBindings(username: username))
|
||||||
|
|
||||||
super.init(initialViewState: viewState)
|
super.init(initialViewState: viewState)
|
||||||
|
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ struct LoginScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
Task { await authenticationService.configure(for: homeserverAddress, flow: .login) }
|
Task { await authenticationService.configure(for: homeserverAddress, flow: .login) }
|
||||||
|
|
||||||
let viewModel = LoginScreenViewModel(authenticationService: authenticationService,
|
let viewModel = LoginScreenViewModel(authenticationService: authenticationService,
|
||||||
|
loginHint: nil,
|
||||||
userIndicatorController: UserIndicatorControllerMock(),
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
analytics: ServiceLocator.shared.analytics)
|
analytics: ServiceLocator.shared.analytics)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class OIDCAuthenticationPresenter: NSObject {
|
|||||||
private let presentationAnchor: UIWindow
|
private let presentationAnchor: UIWindow
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
|
||||||
|
private var activeSession: ASWebAuthenticationSession?
|
||||||
|
|
||||||
init(authenticationService: AuthenticationServiceProtocol,
|
init(authenticationService: AuthenticationServiceProtocol,
|
||||||
oidcRedirectURL: URL,
|
oidcRedirectURL: URL,
|
||||||
presentationAnchor: UIWindow,
|
presentationAnchor: UIWindow,
|
||||||
@@ -36,9 +38,12 @@ class OIDCAuthenticationPresenter: NSObject {
|
|||||||
session.prefersEphemeralWebBrowserSession = false
|
session.prefersEphemeralWebBrowserSession = false
|
||||||
session.presentationContextProvider = self
|
session.presentationContextProvider = self
|
||||||
|
|
||||||
|
activeSession = session
|
||||||
session.start()
|
session.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeSession = nil
|
||||||
|
|
||||||
guard let url else {
|
guard let url else {
|
||||||
// Check for user cancellation to avoid showing an alert in that instance.
|
// Check for user cancellation to avoid showing an alert in that instance.
|
||||||
if let nsError = error as? NSError,
|
if let nsError = error as? NSError,
|
||||||
@@ -76,6 +81,10 @@ class OIDCAuthenticationPresenter: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
activeSession?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
private static let loadingIndicatorID = "\(OIDCAuthenticationPresenter.self)-Loading"
|
private static let loadingIndicatorID = "\(OIDCAuthenticationPresenter.self)-Loading"
|
||||||
|
|
||||||
private func startLoading(delay: Duration? = nil) {
|
private func startLoading(delay: Duration? = nil) {
|
||||||
|
|||||||
+1
-1
@@ -101,7 +101,7 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType,
|
|||||||
startLoading() // Uses the same ID, so no need to worry if the indicator already exists
|
startLoading() // Uses the same ID, so no need to worry if the indicator already exists
|
||||||
defer { stopLoading() }
|
defer { stopLoading() }
|
||||||
|
|
||||||
switch await authenticationService.urlForOIDCLogin() {
|
switch await authenticationService.urlForOIDCLogin(loginHint: nil) {
|
||||||
case .success(let oidcData):
|
case .success(let oidcData):
|
||||||
actionsSubject.send(.continueWithOIDC(data: oidcData, window: window))
|
actionsSubject.send(.continueWithOIDC(data: oidcData, window: window))
|
||||||
case .failure:
|
case .failure:
|
||||||
|
|||||||
+1
-1
@@ -134,7 +134,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
|
|||||||
startLoading()
|
startLoading()
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
switch await authenticationService.urlForOIDCLogin() {
|
switch await authenticationService.urlForOIDCLogin(loginHint: nil) {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
stopLoading()
|
stopLoading()
|
||||||
handleError(error)
|
handleError(error)
|
||||||
|
|||||||
+16
-5
@@ -9,8 +9,11 @@ import Combine
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AuthenticationStartScreenParameters {
|
struct AuthenticationStartScreenParameters {
|
||||||
let showCreateAccountButton: Bool
|
let authenticationService: AuthenticationServiceProtocol
|
||||||
|
let provisioningParameters: AccountProvisioningParameters?
|
||||||
let isBugReportServiceEnabled: Bool
|
let isBugReportServiceEnabled: Bool
|
||||||
|
let appSettings: AppSettings
|
||||||
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
|
final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
|
||||||
@@ -23,8 +26,11 @@ final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(parameters: AuthenticationStartScreenParameters) {
|
init(parameters: AuthenticationStartScreenParameters) {
|
||||||
viewModel = AuthenticationStartScreenViewModel(showCreateAccountButton: parameters.showCreateAccountButton,
|
viewModel = AuthenticationStartScreenViewModel(authenticationService: parameters.authenticationService,
|
||||||
isBugReportServiceEnabled: parameters.isBugReportServiceEnabled)
|
provisioningParameters: parameters.provisioningParameters,
|
||||||
|
isBugReportServiceEnabled: parameters.isBugReportServiceEnabled,
|
||||||
|
appSettings: parameters.appSettings,
|
||||||
|
userIndicatorController: parameters.userIndicatorController)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
@@ -35,14 +41,19 @@ final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .loginManually:
|
|
||||||
actionsSubject.send(.loginManually)
|
|
||||||
case .loginWithQR:
|
case .loginWithQR:
|
||||||
actionsSubject.send(.loginWithQR)
|
actionsSubject.send(.loginWithQR)
|
||||||
|
case .login:
|
||||||
|
actionsSubject.send(.login)
|
||||||
case .register:
|
case .register:
|
||||||
actionsSubject.send(.register)
|
actionsSubject.send(.register)
|
||||||
case .reportProblem:
|
case .reportProblem:
|
||||||
actionsSubject.send(.reportProblem)
|
actionsSubject.send(.reportProblem)
|
||||||
|
|
||||||
|
case .loginDirectlyWithOIDC(let data, let window):
|
||||||
|
actionsSubject.send(.loginDirectlyWithOIDC(data: data, window: window))
|
||||||
|
case .loginDirectlyWithPassword(let loginHint):
|
||||||
|
actionsSubject.send(.loginDirectlyWithPassword(loginHint: loginHint))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
+39
-6
@@ -11,27 +11,60 @@ import SwiftUI
|
|||||||
|
|
||||||
enum AuthenticationStartScreenCoordinatorAction {
|
enum AuthenticationStartScreenCoordinatorAction {
|
||||||
case loginWithQR
|
case loginWithQR
|
||||||
case loginManually
|
case login
|
||||||
case register
|
case register
|
||||||
case reportProblem
|
case reportProblem
|
||||||
|
|
||||||
|
case loginDirectlyWithOIDC(data: OIDCAuthorizationDataProxy, window: UIWindow)
|
||||||
|
case loginDirectlyWithPassword(loginHint: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AuthenticationStartScreenViewModelAction {
|
enum AuthenticationStartScreenViewModelAction: Equatable {
|
||||||
case loginWithQR
|
case loginWithQR
|
||||||
case loginManually
|
case login
|
||||||
case register
|
case register
|
||||||
case reportProblem
|
case reportProblem
|
||||||
|
|
||||||
|
case loginDirectlyWithOIDC(data: OIDCAuthorizationDataProxy, window: UIWindow)
|
||||||
|
case loginDirectlyWithPassword(loginHint: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AuthenticationStartScreenViewState: BindableState {
|
struct AuthenticationStartScreenViewState: BindableState {
|
||||||
|
/// The presentation anchor used for OIDC authentication.
|
||||||
|
var window: UIWindow?
|
||||||
|
|
||||||
|
let serverName: String?
|
||||||
let showCreateAccountButton: Bool
|
let showCreateAccountButton: Bool
|
||||||
let isQRCodeLoginEnabled: Bool
|
let showQRCodeLoginButton: Bool
|
||||||
let isBugReportServiceEnabled: Bool
|
let showReportProblemButton: Bool
|
||||||
|
|
||||||
|
var bindings = AuthenticationStartScreenViewStateBindings()
|
||||||
|
|
||||||
|
var loginButtonTitle: String {
|
||||||
|
if let serverName {
|
||||||
|
L10n.screenOnboardingSignInTo(serverName)
|
||||||
|
} else if showQRCodeLoginButton {
|
||||||
|
L10n.screenOnboardingSignInManually
|
||||||
|
} else {
|
||||||
|
L10n.actionContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AuthenticationStartScreenViewStateBindings {
|
||||||
|
var alertInfo: AlertInfo<AuthenticationStartScreenAlertType>?
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuthenticationStartScreenAlertType {
|
||||||
|
case genericError
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AuthenticationStartScreenViewAction {
|
enum AuthenticationStartScreenViewAction {
|
||||||
|
/// Updates the window used as the OIDC presentation anchor.
|
||||||
|
case updateWindow(UIWindow)
|
||||||
|
|
||||||
case loginWithQR
|
case loginWithQR
|
||||||
case loginManually
|
case login
|
||||||
case register
|
case register
|
||||||
case reportProblem
|
case reportProblem
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-6
@@ -11,28 +11,105 @@ import SwiftUI
|
|||||||
typealias AuthenticationStartScreenViewModelType = StateStoreViewModelV2<AuthenticationStartScreenViewState, AuthenticationStartScreenViewAction>
|
typealias AuthenticationStartScreenViewModelType = StateStoreViewModelV2<AuthenticationStartScreenViewState, AuthenticationStartScreenViewAction>
|
||||||
|
|
||||||
class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType, AuthenticationStartScreenViewModelProtocol {
|
class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType, AuthenticationStartScreenViewModelProtocol {
|
||||||
|
private let authenticationService: AuthenticationServiceProtocol
|
||||||
|
private let provisioningParameters: AccountProvisioningParameters?
|
||||||
|
private let appSettings: AppSettings
|
||||||
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
|
||||||
private var actionsSubject: PassthroughSubject<AuthenticationStartScreenViewModelAction, Never> = .init()
|
private var actionsSubject: PassthroughSubject<AuthenticationStartScreenViewModelAction, Never> = .init()
|
||||||
|
|
||||||
var actions: AnyPublisher<AuthenticationStartScreenViewModelAction, Never> {
|
var actions: AnyPublisher<AuthenticationStartScreenViewModelAction, Never> {
|
||||||
actionsSubject.eraseToAnyPublisher()
|
actionsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(showCreateAccountButton: Bool, isBugReportServiceEnabled: Bool) {
|
init(authenticationService: AuthenticationServiceProtocol,
|
||||||
super.init(initialViewState: AuthenticationStartScreenViewState(showCreateAccountButton: showCreateAccountButton,
|
provisioningParameters: AccountProvisioningParameters?,
|
||||||
isQRCodeLoginEnabled: !ProcessInfo.processInfo.isiOSAppOnMac,
|
isBugReportServiceEnabled: Bool,
|
||||||
isBugReportServiceEnabled: isBugReportServiceEnabled))
|
appSettings: AppSettings,
|
||||||
|
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||||
|
self.authenticationService = authenticationService
|
||||||
|
self.provisioningParameters = provisioningParameters
|
||||||
|
self.appSettings = appSettings
|
||||||
|
self.userIndicatorController = userIndicatorController
|
||||||
|
|
||||||
|
// We only show the "Sign in to …" button when using a provisioning link.
|
||||||
|
let showCreateAccountButton = appSettings.showCreateAccountButton && provisioningParameters == nil
|
||||||
|
let showQRCodeLoginButton = !ProcessInfo.processInfo.isiOSAppOnMac && provisioningParameters == nil
|
||||||
|
|
||||||
|
super.init(initialViewState: AuthenticationStartScreenViewState(serverName: provisioningParameters?.accountProvider,
|
||||||
|
showCreateAccountButton: showCreateAccountButton,
|
||||||
|
showQRCodeLoginButton: showQRCodeLoginButton,
|
||||||
|
showReportProblemButton: isBugReportServiceEnabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
override func process(viewAction: AuthenticationStartScreenViewAction) {
|
override func process(viewAction: AuthenticationStartScreenViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
|
case .updateWindow(let window):
|
||||||
|
guard state.window != window else { return }
|
||||||
|
state.window = window
|
||||||
case .loginWithQR:
|
case .loginWithQR:
|
||||||
actionsSubject.send(.loginWithQR)
|
actionsSubject.send(.loginWithQR)
|
||||||
case .loginManually:
|
case .login:
|
||||||
actionsSubject.send(.loginManually)
|
Task { await login() }
|
||||||
case .register:
|
case .register:
|
||||||
actionsSubject.send(.register)
|
actionsSubject.send(.register)
|
||||||
case .reportProblem:
|
case .reportProblem:
|
||||||
actionsSubject.send(.reportProblem)
|
actionsSubject.send(.reportProblem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func login() async {
|
||||||
|
if let provisioningParameters {
|
||||||
|
await configureProvisionedServer(with: provisioningParameters)
|
||||||
|
} else {
|
||||||
|
actionsSubject.send(.login) // No need to configure anything here, continue the flow.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureProvisionedServer(with provisioningParameters: AccountProvisioningParameters) async {
|
||||||
|
startLoading()
|
||||||
|
defer { stopLoading() }
|
||||||
|
|
||||||
|
guard case .success = await authenticationService.configure(for: provisioningParameters.accountProvider, flow: .login) else {
|
||||||
|
// As the server was provisioned, we don't worry about the specifics and show a generic error to the user.
|
||||||
|
displayError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard authenticationService.homeserver.value.loginMode.supportsOIDCFlow else {
|
||||||
|
actionsSubject.send(.loginDirectlyWithPassword(loginHint: provisioningParameters.loginHint))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let window = state.window else {
|
||||||
|
displayError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch await authenticationService.urlForOIDCLogin(loginHint: provisioningParameters.loginHint) {
|
||||||
|
case .success(let oidcData):
|
||||||
|
actionsSubject.send(.loginDirectlyWithOIDC(data: oidcData, window: window))
|
||||||
|
case .failure:
|
||||||
|
displayError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let loadingIndicatorID = "\(AuthenticationStartScreenViewModel.self)-Loading"
|
||||||
|
|
||||||
|
private func startLoading() {
|
||||||
|
userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorID,
|
||||||
|
type: .modal,
|
||||||
|
title: L10n.commonLoading,
|
||||||
|
persistent: true))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopLoading() {
|
||||||
|
userIndicatorController.retractIndicatorWithId(loadingIndicatorID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func displayError() {
|
||||||
|
state.bindings.alertInfo = AlertInfo(id: .genericError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-7
@@ -35,7 +35,7 @@ struct AuthenticationStartScreen: View {
|
|||||||
}
|
}
|
||||||
.frame(maxHeight: .infinity)
|
.frame(maxHeight: .infinity)
|
||||||
.safeAreaInset(edge: .bottom) {
|
.safeAreaInset(edge: .bottom) {
|
||||||
if context.viewState.isBugReportServiceEnabled {
|
if context.viewState.showReportProblemButton {
|
||||||
Button {
|
Button {
|
||||||
context.send(viewAction: .reportProblem)
|
context.send(viewAction: .reportProblem)
|
||||||
} label: {
|
} label: {
|
||||||
@@ -45,6 +45,7 @@ struct AuthenticationStartScreen: View {
|
|||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
}
|
}
|
||||||
.frame(width: geometry.size.width)
|
.frame(width: geometry.size.width)
|
||||||
|
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.reportAProblem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +53,9 @@ struct AuthenticationStartScreen: View {
|
|||||||
.background {
|
.background {
|
||||||
AuthenticationStartScreenBackgroundImage()
|
AuthenticationStartScreenBackgroundImage()
|
||||||
}
|
}
|
||||||
|
.introspect(.window, on: .supportedVersions) { window in
|
||||||
|
context.send(viewAction: .updateWindow(window))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var content: some View {
|
var content: some View {
|
||||||
@@ -89,7 +93,7 @@ struct AuthenticationStartScreen: View {
|
|||||||
/// The main action buttons.
|
/// The main action buttons.
|
||||||
var buttons: some View {
|
var buttons: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
if context.viewState.isQRCodeLoginEnabled {
|
if context.viewState.showQRCodeLoginButton {
|
||||||
Button { context.send(viewAction: .loginWithQR) } label: {
|
Button { context.send(viewAction: .loginWithQR) } label: {
|
||||||
Label(L10n.screenOnboardingSignInWithQrCode, icon: \.qrCode)
|
Label(L10n.screenOnboardingSignInWithQrCode, icon: \.qrCode)
|
||||||
}
|
}
|
||||||
@@ -97,8 +101,8 @@ struct AuthenticationStartScreen: View {
|
|||||||
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.signInWithQr)
|
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.signInWithQr)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button { context.send(viewAction: .loginManually) } label: {
|
Button { context.send(viewAction: .login) } label: {
|
||||||
Text(context.viewState.isQRCodeLoginEnabled ? L10n.screenOnboardingSignInManually : L10n.actionContinue)
|
Text(context.viewState.loginButtonTitle)
|
||||||
}
|
}
|
||||||
.buttonStyle(.compound(.primary))
|
.buttonStyle(.compound(.primary))
|
||||||
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.signIn)
|
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.signIn)
|
||||||
@@ -120,16 +124,22 @@ struct AuthenticationStartScreen: View {
|
|||||||
struct AuthenticationStartScreen_Previews: PreviewProvider, TestablePreview {
|
struct AuthenticationStartScreen_Previews: PreviewProvider, TestablePreview {
|
||||||
static let viewModel = makeViewModel()
|
static let viewModel = makeViewModel()
|
||||||
static let bugReportDisabledViewModel = makeViewModel(isBugReportServiceEnabled: false)
|
static let bugReportDisabledViewModel = makeViewModel(isBugReportServiceEnabled: false)
|
||||||
|
static let provisionedViewModel = makeViewModel(provisionedServerName: "example.com")
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
AuthenticationStartScreen(context: viewModel.context)
|
AuthenticationStartScreen(context: viewModel.context)
|
||||||
.previewDisplayName("Default")
|
.previewDisplayName("Default")
|
||||||
AuthenticationStartScreen(context: bugReportDisabledViewModel.context)
|
AuthenticationStartScreen(context: bugReportDisabledViewModel.context)
|
||||||
.previewDisplayName("Bug report disabled")
|
.previewDisplayName("Bug report disabled")
|
||||||
|
AuthenticationStartScreen(context: provisionedViewModel.context)
|
||||||
|
.previewDisplayName("Provisioned")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeViewModel(isBugReportServiceEnabled: Bool = true) -> AuthenticationStartScreenViewModel {
|
static func makeViewModel(isBugReportServiceEnabled: Bool = true, provisionedServerName: String? = nil) -> AuthenticationStartScreenViewModel {
|
||||||
AuthenticationStartScreenViewModel(showCreateAccountButton: true,
|
AuthenticationStartScreenViewModel(authenticationService: AuthenticationService.mock,
|
||||||
isBugReportServiceEnabled: isBugReportServiceEnabled)
|
provisioningParameters: provisionedServerName.map { .init(accountProvider: $0, loginHint: nil) },
|
||||||
|
isBugReportServiceEnabled: isBugReportServiceEnabled,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ struct BugReportScreen: View {
|
|||||||
Button(L10n.actionCancel) {
|
Button(L10n.actionCancel) {
|
||||||
context.send(viewAction: .cancel)
|
context.send(viewAction: .cancel)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier(A11yIdentifiers.bugReportScreen.cancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,13 +82,14 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func urlForOIDCLogin() async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError> {
|
func urlForOIDCLogin(loginHint: String?) async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError> {
|
||||||
guard let client else { return .failure(.oidcError(.urlFailure)) }
|
guard let client else { return .failure(.oidcError(.urlFailure)) }
|
||||||
do {
|
do {
|
||||||
// The create prompt is broken: https://github.com/element-hq/matrix-authentication-service/issues/3429
|
// The create prompt is broken: https://github.com/element-hq/matrix-authentication-service/issues/3429
|
||||||
// let prompt: OidcPrompt = flow == .register ? .create : .consent
|
// let prompt: OidcPrompt = flow == .register ? .create : .consent
|
||||||
let oidcData = try await client.urlForOidc(oidcConfiguration: appSettings.oidcConfiguration.rustValue,
|
let oidcData = try await client.urlForOidc(oidcConfiguration: appSettings.oidcConfiguration.rustValue,
|
||||||
prompt: .consent)
|
prompt: .consent,
|
||||||
|
loginHint: loginHint)
|
||||||
return .success(OIDCAuthorizationDataProxy(underlyingData: oidcData))
|
return .success(OIDCAuthorizationDataProxy(underlyingData: oidcData))
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Failed to get URL for OIDC login: \(error)")
|
MXLog.error("Failed to get URL for OIDC login: \(error)")
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ protocol AuthenticationServiceProtocol {
|
|||||||
/// Sets up the service for login on the specified homeserver address.
|
/// Sets up the service for login on the specified homeserver address.
|
||||||
func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError>
|
func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError>
|
||||||
/// Performs login using OIDC for the current homeserver.
|
/// Performs login using OIDC for the current homeserver.
|
||||||
func urlForOIDCLogin() async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError>
|
func urlForOIDCLogin(loginHint: String?) async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError>
|
||||||
/// Asks the SDK to abort an ongoing OIDC login if we didn't get a callback to complete the request with.
|
/// Asks the SDK to abort an ongoing OIDC login if we didn't get a callback to complete the request with.
|
||||||
func abortOIDCLogin(data: OIDCAuthorizationDataProxy) async
|
func abortOIDCLogin(data: OIDCAuthorizationDataProxy) async
|
||||||
/// Completes an OIDC login that was started using ``urlForOIDCLogin``.
|
/// Completes an OIDC login that was started using ``urlForOIDCLogin``.
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class MockScreen: Identifiable {
|
|||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController))
|
userIndicatorController: ServiceLocator.shared.userIndicatorController))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .authenticationFlow:
|
case .authenticationFlow, .provisionedAuthenticationFlow:
|
||||||
let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: AuthenticationService.mock,
|
let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: AuthenticationService.mock,
|
||||||
qrCodeLoginService: QRCodeLoginServiceMock(),
|
qrCodeLoginService: QRCodeLoginServiceMock(),
|
||||||
bugReportService: BugReportServiceMock(.init()),
|
bugReportService: BugReportServiceMock(.init()),
|
||||||
@@ -131,6 +131,11 @@ class MockScreen: Identifiable {
|
|||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
flowCoordinator.start()
|
flowCoordinator.start()
|
||||||
retainedState.append(flowCoordinator)
|
retainedState.append(flowCoordinator)
|
||||||
|
|
||||||
|
if id == .provisionedAuthenticationFlow {
|
||||||
|
flowCoordinator.handleAppRoute(.accountProvisioningLink(.init(accountProvider: "example.com", loginHint: nil)), animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case .appLockFlow, .appLockFlowDisabled:
|
case .appLockFlow, .appLockFlowDisabled:
|
||||||
// The tested coordinator is setup below in the alternate window.
|
// The tested coordinator is setup below in the alternate window.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ enum UITestsScreenIdentifier: String {
|
|||||||
case appLockSetupFlowMandatory
|
case appLockSetupFlowMandatory
|
||||||
case appLockSetupFlowUnlock
|
case appLockSetupFlowUnlock
|
||||||
case authenticationFlow
|
case authenticationFlow
|
||||||
|
case provisionedAuthenticationFlow
|
||||||
case bugReport
|
case bugReport
|
||||||
case createPoll
|
case createPoll
|
||||||
case createRoom
|
case createRoom
|
||||||
|
|||||||
+1
-1
Submodule Enterprise updated: c11c3af9a8...e004345aa5
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ea83e380f7edc4b64fc32bf9efe3e646a55e99674b2c23c0f6133c4068478735
|
||||||
|
size 99912
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ae0ab1353481ccf9413276e74e4ed133e40d1fcdb3a098a9aaa2109ac782edee
|
||||||
|
size 118205
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:23d5b99dffbf13745655e85d5710c600458786ee7006a5cc5595e760c9beecb8
|
||||||
|
size 532099
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8e07f75e8576371d2937b9595da7b9473261c8ea1deab9ad7aca3aa686ea510d
|
||||||
|
size 559743
|
||||||
@@ -13,6 +13,9 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase {
|
|||||||
// Given the authentication flow.
|
// Given the authentication flow.
|
||||||
let app = Application.launch(.authenticationFlow)
|
let app = Application.launch(.authenticationFlow)
|
||||||
|
|
||||||
|
// Check the bug report flow works.
|
||||||
|
try await verifyReportBugButton(app)
|
||||||
|
|
||||||
// Splash Screen: Tap get started button
|
// Splash Screen: Tap get started button
|
||||||
app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap()
|
app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap()
|
||||||
|
|
||||||
@@ -144,4 +147,38 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertTrue(wasAlertText.exists, "The web authentication prompt should be shown after selecting a homeserver with OIDC.")
|
XCTAssertTrue(wasAlertText.exists, "The web authentication prompt should be shown after selecting a homeserver with OIDC.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testProvisionedLoginWithPassword() async throws {
|
||||||
|
// Given a provisioned authentication flow.
|
||||||
|
let app = Application.launch(.provisionedAuthenticationFlow)
|
||||||
|
|
||||||
|
// Then the start screen should be configured appropriately.
|
||||||
|
try await app.assertScreenshot()
|
||||||
|
|
||||||
|
// Check the bug report flow works.
|
||||||
|
try await verifyReportBugButton(app)
|
||||||
|
|
||||||
|
// Splash Screen: Tap get started button
|
||||||
|
app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap()
|
||||||
|
|
||||||
|
// Login Screen: Wait for continue button to appear
|
||||||
|
let continueButton = app.buttons[A11yIdentifiers.loginScreen.continue]
|
||||||
|
XCTAssertTrue(continueButton.waitForExistence(timeout: 2.0))
|
||||||
|
|
||||||
|
// Login Screen: Enter valid credentials
|
||||||
|
app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("alice\n", app: app)
|
||||||
|
app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("12345678", app: app)
|
||||||
|
|
||||||
|
// Login Screen: Tap next
|
||||||
|
app.buttons[A11yIdentifiers.loginScreen.continue].tap()
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyReportBugButton(_ app: XCUIApplication) async throws {
|
||||||
|
// Splash Screen: Report a problem button.
|
||||||
|
app.buttons[A11yIdentifiers.authenticationStartScreen.reportAProblem].tap()
|
||||||
|
|
||||||
|
// Bug report: Make sure it exists then cancel.
|
||||||
|
XCTAssert(app.textFields[A11yIdentifiers.bugReportScreen.report].exists)
|
||||||
|
app.buttons[A11yIdentifiers.bugReportScreen.cancel].tap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:eea5297a9bded2679b86a8884ddab8e6ebb83ea41d9373759fa38c31cca05e34
|
||||||
|
size 1371559
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:827b57b732448bab45b2bae268dad31c48b9576e3a9d96a4a382e0ce2e7aa531
|
||||||
|
size 1210036
|
||||||
@@ -9,6 +9,117 @@ import XCTest
|
|||||||
|
|
||||||
@testable import ElementX
|
@testable import ElementX
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class AuthenticationStartScreenViewModelTests: XCTestCase {
|
class AuthenticationStartScreenViewModelTests: XCTestCase {
|
||||||
// Nothing to test, the view model has no mutable state.
|
var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
|
||||||
|
var client: ClientSDKMock!
|
||||||
|
var authenticationService: AuthenticationServiceProtocol!
|
||||||
|
|
||||||
|
var viewModel: AuthenticationStartScreenViewModel!
|
||||||
|
var context: AuthenticationStartScreenViewModel.Context { viewModel.context }
|
||||||
|
|
||||||
|
func testInitialState() async throws {
|
||||||
|
// Given a view model that has no provisioning parameters.
|
||||||
|
setupViewModel()
|
||||||
|
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||||
|
|
||||||
|
// When tapping any of the buttons on the screen
|
||||||
|
let actions: [(AuthenticationStartScreenViewAction, AuthenticationStartScreenViewModelAction)] = [
|
||||||
|
(.loginWithQR, .loginWithQR),
|
||||||
|
(.login, .login),
|
||||||
|
(.register, .register),
|
||||||
|
(.reportProblem, .reportProblem)
|
||||||
|
]
|
||||||
|
|
||||||
|
for action in actions {
|
||||||
|
let deferred = deferFulfillment(viewModel.actions) { $0 == action.1 }
|
||||||
|
context.send(viewAction: action.0)
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
// Then the authentication service should not be used yet.
|
||||||
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||||
|
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testProvisionedOIDCState() async throws {
|
||||||
|
// Given a view model that has been provisioned with a server that supports OIDC.
|
||||||
|
setupViewModel(provisioningParameters: .init(accountProvider: "company.com", loginHint: "user@company.com"))
|
||||||
|
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||||
|
|
||||||
|
// When tapping the login button the authentication service should be used and the screen
|
||||||
|
// should request to continue the flow without any server selection needed.
|
||||||
|
let deferred = deferFulfillment(viewModel.actions) { $0.isLoginDirectlyWithOIDC }
|
||||||
|
context.send(viewAction: .login)
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 1)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintReceivedArguments?.prompt, .consent)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintReceivedArguments?.loginHint, "user@company.com")
|
||||||
|
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .oidc(supportsCreatePrompt: false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testProvisionedPasswordState() async throws {
|
||||||
|
// Given a view model that has been provisioned with a server that does not support OIDC.
|
||||||
|
setupViewModel(provisioningParameters: .init(accountProvider: "company.com", loginHint: "user@company.com"), supportsOIDC: false)
|
||||||
|
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||||
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||||
|
|
||||||
|
// When tapping the login button the authentication service should be used and the screen
|
||||||
|
// should request to continue the flow without any server selection needed.
|
||||||
|
let deferred = deferFulfillment(viewModel.actions) { $0.isLoginDirectlyWithPassword }
|
||||||
|
context.send(viewAction: .login)
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
// Then a call to configure service should be made.
|
||||||
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
|
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private func setupViewModel(provisioningParameters: AccountProvisioningParameters? = nil, supportsOIDC: Bool = true) {
|
||||||
|
// Manually create a configuration as the default homeserver address setting is immutable.
|
||||||
|
client = ClientSDKMock(configuration: .init(oidcLoginURL: supportsOIDC ? "https://account.company.com/authorize" : nil,
|
||||||
|
supportsOIDCCreatePrompt: false,
|
||||||
|
supportsPasswordLogin: true))
|
||||||
|
let configuration = AuthenticationClientBuilderMock.Configuration(homeserverClients: ["company.com": client],
|
||||||
|
qrCodeClient: client)
|
||||||
|
|
||||||
|
clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init(builderConfiguration: configuration))
|
||||||
|
authenticationService = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||||
|
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||||
|
clientBuilderFactory: clientBuilderFactory,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
appHooks: AppHooks())
|
||||||
|
|
||||||
|
viewModel = AuthenticationStartScreenViewModel(authenticationService: authenticationService,
|
||||||
|
provisioningParameters: provisioningParameters,
|
||||||
|
isBugReportServiceEnabled: true,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|
||||||
|
// Add a fake window in order for the OIDC flow to continue
|
||||||
|
viewModel.context.send(viewAction: .updateWindow(UIWindow()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AuthenticationStartScreenViewModelAction {
|
||||||
|
var isLoginDirectlyWithOIDC: Bool {
|
||||||
|
switch self {
|
||||||
|
case .loginDirectlyWithOIDC: true
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isLoginDirectlyWithPassword: Bool {
|
||||||
|
switch self {
|
||||||
|
case .loginDirectlyWithPassword: true
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,24 +17,6 @@ class LoginScreenViewModelTests: XCTestCase {
|
|||||||
var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
|
var clientBuilderFactory: AuthenticationClientBuilderFactoryMock!
|
||||||
var service: AuthenticationServiceProtocol!
|
var service: AuthenticationServiceProtocol!
|
||||||
|
|
||||||
private func setupViewModel(homeserverAddress: String = "example.com") async {
|
|
||||||
clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init())
|
|
||||||
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
|
||||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
|
||||||
clientBuilderFactory: clientBuilderFactory,
|
|
||||||
appSettings: ServiceLocator.shared.settings,
|
|
||||||
appHooks: AppHooks())
|
|
||||||
|
|
||||||
guard case .success = await service.configure(for: homeserverAddress, flow: .login) else {
|
|
||||||
XCTFail("A valid server should be configured for the test.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel = LoginScreenViewModel(authenticationService: service,
|
|
||||||
userIndicatorController: UserIndicatorControllerMock(),
|
|
||||||
analytics: ServiceLocator.shared.analytics)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testBasicServer() async {
|
func testBasicServer() async {
|
||||||
// Given the view model configured for a basic server example.com that only supports password authentication.
|
// Given the view model configured for a basic server example.com that only supports password authentication.
|
||||||
await setupViewModel()
|
await setupViewModel()
|
||||||
@@ -160,4 +142,36 @@ class LoginScreenViewModelTests: XCTestCase {
|
|||||||
// Then the view state should be updated to show an alert.
|
// Then the view state should be updated to show an alert.
|
||||||
XCTAssertEqual(context.alertInfo?.id, .unknown, "An alert should be shown to the user.")
|
XCTAssertEqual(context.alertInfo?.id, .unknown, "An alert should be shown to the user.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testLoginHint() async throws {
|
||||||
|
await setupViewModel(loginHint: "")
|
||||||
|
XCTAssertEqual(context.username, "")
|
||||||
|
|
||||||
|
await setupViewModel(loginHint: "alice")
|
||||||
|
XCTAssertEqual(context.username, "alice")
|
||||||
|
|
||||||
|
await setupViewModel(loginHint: "mxid:@alice:example.com")
|
||||||
|
XCTAssertEqual(context.username, "@alice:example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private func setupViewModel(homeserverAddress: String = "example.com", loginHint: String? = nil) async {
|
||||||
|
clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init())
|
||||||
|
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||||
|
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||||
|
clientBuilderFactory: clientBuilderFactory,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
appHooks: AppHooks())
|
||||||
|
|
||||||
|
guard case .success = await service.configure(for: homeserverAddress, flow: .login) else {
|
||||||
|
XCTFail("A valid server should be configured for the test.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel = LoginScreenViewModel(authenticationService: service,
|
||||||
|
loginHint: loginHint,
|
||||||
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
|
analytics: ServiceLocator.shared.analytics)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
setupViewModel(authenticationFlow: .login)
|
setupViewModel(authenticationFlow: .login)
|
||||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||||
|
|
||||||
// When continuing from the confirmation screen.
|
// When continuing from the confirmation screen.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||||
@@ -32,8 +32,8 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
// Then a call to configure service should be made.
|
// Then a call to configure service should be made.
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 1)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 1)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .consent)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintReceivedArguments?.prompt, .consent)
|
||||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||||
|
|
||||||
// When continuing from the confirmation screen.
|
// When continuing from the confirmation screen.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||||
@@ -55,8 +55,8 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
// Then the configured homeserver should be used and no additional client should be built.
|
// Then the configured homeserver should be used and no additional client should be built.
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 1)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 1)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .consent)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintReceivedArguments?.prompt, .consent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testConfirmRegisterWithoutConfiguration() async throws {
|
func testConfirmRegisterWithoutConfiguration() async throws {
|
||||||
@@ -64,7 +64,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
setupViewModel(authenticationFlow: .register)
|
setupViewModel(authenticationFlow: .register)
|
||||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||||
|
|
||||||
// When continuing from the confirmation screen.
|
// When continuing from the confirmation screen.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||||
@@ -73,7 +73,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
// Then a call to configure service should be made.
|
// Then a call to configure service should be made.
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 1)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 1)
|
||||||
// The create prompt is broken: https://github.com/element-hq/matrix-authentication-service/issues/3429
|
// The create prompt is broken: https://github.com/element-hq/matrix-authentication-service/issues/3429
|
||||||
// XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .create)
|
// XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .create)
|
||||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||||
@@ -88,7 +88,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
XCTAssertEqual(service.homeserver.value.loginMode, .oidc(supportsCreatePrompt: true))
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||||
|
|
||||||
// When continuing from the confirmation screen.
|
// When continuing from the confirmation screen.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithOIDC }
|
||||||
@@ -99,7 +99,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
// The create prompt is broken: https://github.com/element-hq/matrix-authentication-service/issues/3429
|
// The create prompt is broken: https://github.com/element-hq/matrix-authentication-service/issues/3429
|
||||||
// XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .create)
|
// XCTAssertEqual(client.urlForOidcOidcConfigurationPromptReceivedArguments?.prompt, .create)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 1)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testConfirmPasswordLoginWithoutConfiguration() async throws {
|
func testConfirmPasswordLoginWithoutConfiguration() async throws {
|
||||||
@@ -107,7 +107,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
setupViewModel(authenticationFlow: .login, supportsOIDC: false)
|
setupViewModel(authenticationFlow: .login, supportsOIDC: false)
|
||||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||||
|
|
||||||
// When continuing from the confirmation screen.
|
// When continuing from the confirmation screen.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
||||||
@@ -116,7 +116,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
// Then a call to configure service should be made, but not for the OIDC URL.
|
// Then a call to configure service should be made, but not for the OIDC URL.
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||||
XCTAssertEqual(service.homeserver.value.loginMode, .password)
|
XCTAssertEqual(service.homeserver.value.loginMode, .password)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
XCTAssertEqual(service.homeserver.value.loginMode, .password)
|
XCTAssertEqual(service.homeserver.value.loginMode, .password)
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||||
|
|
||||||
// When continuing from the confirmation screen.
|
// When continuing from the confirmation screen.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isContinueWithPassword }
|
||||||
@@ -138,7 +138,7 @@ class ServerConfirmationScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
// Then the configured homeserver should be used and no additional client should be built, nor a call to get the OIDC URL.
|
// Then the configured homeserver should be used and no additional client should be built, nor a call to get the OIDC URL.
|
||||||
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptCallsCount, 0)
|
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintCallsCount, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRegistrationNotSupportedAlert() async throws {
|
func testRegistrationNotSupportedAlert() async throws {
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ packages:
|
|||||||
# Element/Matrix dependencies
|
# Element/Matrix dependencies
|
||||||
MatrixRustSDK:
|
MatrixRustSDK:
|
||||||
url: https://github.com/element-hq/matrix-rust-components-swift
|
url: https://github.com/element-hq/matrix-rust-components-swift
|
||||||
exactVersion: 25.05.07
|
exactVersion: 25.05.09
|
||||||
# path: ../matrix-rust-sdk
|
# path: ../matrix-rust-sdk
|
||||||
Compound:
|
Compound:
|
||||||
url: https://github.com/element-hq/compound-ios
|
url: https://github.com/element-hq/compound-ios
|
||||||
|
|||||||
Reference in New Issue
Block a user