diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 2cb435857..405afdf5f 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -231,6 +231,7 @@ 518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */; }; 51C240F4660F7269203A9B3A /* MigrationScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75910F5A36EA8FF9BAD08D18 /* MigrationScreenUITests.swift */; }; 520EEDAFBC778AB0B41F2F53 /* ClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE6170EFE6A161B0A68AB61 /* ClientMock.swift */; }; + 523C6800ED85D5810CF18C19 /* OIDCAccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */; }; 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; 53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */; }; 53A59720F4729D9BBFFB7CAB /* NotificationSettingsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9CB3B9DFA353AB2B7CD9F8 /* NotificationSettingsEditScreenCoordinator.swift */; }; @@ -1372,6 +1373,7 @@ C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = ""; }; C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationModeProxy.swift; sourceTree = ""; }; C15E0017717EAE3A1D02D005 /* StaticLocationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenCoordinator.swift; sourceTree = ""; }; + C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCAccountSettingsPresenter.swift; sourceTree = ""; }; C23B3FAD8B23C421BC0D1B1E /* MapTilerGeoCodingServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerGeoCodingServiceProtocol.swift; sourceTree = ""; }; C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = ""; }; C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreen.swift; sourceTree = ""; }; @@ -2499,6 +2501,7 @@ 70B74A432C241E56A7ACE610 /* Settings */ = { isa = PBXGroup; children = ( + EB5B1119B5AD79297F1D49EB /* AccountSettings */, 09C599CB430ABF160C1EE55C /* AnalyticsSettingsScreen */, 1CA6CD0DE6F0445156361B6D /* DeveloperOptionsScreen */, 38A1C74493B816B8753F5BC2 /* LegalInformationScreen */, @@ -3691,6 +3694,14 @@ path = View; sourceTree = ""; }; + EB5B1119B5AD79297F1D49EB /* AccountSettings */ = { + isa = PBXGroup; + children = ( + C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */, + ); + path = AccountSettings; + sourceTree = ""; + }; EBBEB5471737E9D116DF4738 /* Background */ = { isa = PBXGroup; children = ( @@ -4594,6 +4605,7 @@ D12F440F7973F1489F61389D /* NotificationSettingsScreenModels.swift in Sources */, 7F7EA51A9A43125A8CB6AC90 /* NotificationSettingsScreenViewModel.swift in Sources */, CBD2ABE4C1A47ECD99E1488E /* NotificationSettingsScreenViewModelProtocol.swift in Sources */, + 523C6800ED85D5810CF18C19 /* OIDCAccountSettingsPresenter.swift in Sources */, 9A4E3D5AA44B041DAC3A0D81 /* OIDCAuthenticationPresenter.swift in Sources */, 329571083B132E4941131835 /* OnboardingBackgroundImage.swift in Sources */, 2CB6787E25B11711518E9588 /* OnboardingCoordinator.swift in Sources */, @@ -5507,7 +5519,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = "1.0.110-alpha"; + version = "1.0.112-alpha"; }; }; 821C67C9A7F8CC3FD41B28B4 /* XCRemoteSwiftPackageReference "emojibase-bindings" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0da5651ac..14ea18caf 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -129,8 +129,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-rust-components-swift", "state" : { - "revision" : "d1a47b07b52012cd2bdad910488c0249003e05ce", - "version" : "1.0.110-alpha" + "revision" : "32631740a23b8e7a23dcbc919bffc16ba3c3d270", + "version" : "1.0.112-alpha" } }, { diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index af12d729b..c982677dc 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -372,6 +372,7 @@ "screen_session_verification_they_match" = "They match"; "screen_session_verification_waiting_to_accept_subtitle" = "Accept the request to start the verification process in your other session to continue."; "screen_session_verification_waiting_to_accept_title" = "Waiting to accept request"; +"screen_settings_oidc_account" = "Account and devices"; "screen_share_location_title" = "Share location"; "screen_share_my_location_action" = "Share my location"; "screen_share_open_apple_maps" = "Open in Apple Maps"; diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 6dd75746a..c95f03ae4 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -543,11 +543,9 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, } private func startSync() { - ServiceLocator.shared.analytics.signpost.beginSync() - guard let userSession else { - fatalError("User session not setup") - } + guard let userSession else { return } + ServiceLocator.shared.analytics.signpost.beginSync() userSession.clientProxy.startSync() let identifier = "StaleDataIndicator" diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 03fb39841..d8b5f60d8 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -938,6 +938,8 @@ public enum L10n { public static var screenSessionVerificationWaitingToAcceptSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_waiting_to_accept_subtitle") } /// Waiting to accept request public static var screenSessionVerificationWaitingToAcceptTitle: String { return L10n.tr("Localizable", "screen_session_verification_waiting_to_accept_title") } + /// Account and devices + public static var screenSettingsOidcAccount: String { return L10n.tr("Localizable", "screen_settings_oidc_account") } /// Share location public static var screenShareLocationTitle: String { return L10n.tr("Localizable", "screen_share_location_title") } /// Share my location diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index f128add45..512fa1ab8 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -31,6 +31,23 @@ class SDKClientMock: SDKClientProtocol { return accountDataEventTypeReturnValue } } + //MARK: - `accountUrl` + + public var accountUrlCallsCount = 0 + public var accountUrlCalled: Bool { + return accountUrlCallsCount > 0 + } + public var accountUrlReturnValue: String? + public var accountUrlClosure: (() -> String?)? + + public func `accountUrl`() -> String? { + accountUrlCallsCount += 1 + if let accountUrlClosure = accountUrlClosure { + return accountUrlClosure() + } else { + return accountUrlReturnValue + } + } //MARK: - `avatarUrl` public var avatarUrlThrowableError: Error? @@ -367,14 +384,19 @@ class SDKClientMock: SDKClientProtocol { public var logoutCalled: Bool { return logoutCallsCount > 0 } - public var logoutClosure: (() throws -> Void)? + public var logoutReturnValue: String? + public var logoutClosure: (() throws -> String?)? - public func `logout`() throws { + public func `logout`() throws -> String? { if let error = logoutThrowableError { throw error } logoutCallsCount += 1 - try logoutClosure?() + if let logoutClosure = logoutClosure { + return try logoutClosure() + } else { + return logoutReturnValue + } } //MARK: - `notificationClient` diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift index f22827217..9a3f73504 100644 --- a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift @@ -100,7 +100,16 @@ class AuthenticationCoordinator: CoordinatorProtocol { if isModallyPresented { navigationStackCoordinator.setSheetCoordinator(nil) } else { - showLoginScreen() + // We are here because the default server failed to respond. + if authenticationService.homeserver.value.loginMode == .password { + // Add the password login screen directly to the flow, its fine. + showLoginScreen() + } else { + // OIDC is presented from the confirmation screen so replace the + // server selection screen which was inserted to handle the failure. + navigationStackCoordinator.pop() + showServerConfirmationScreen() + } } case .dismiss: navigationStackCoordinator.setSheetCoordinator(nil) diff --git a/ElementX/Sources/Screens/Settings/AccountSettings/OIDCAccountSettingsPresenter.swift b/ElementX/Sources/Screens/Settings/AccountSettings/OIDCAccountSettingsPresenter.swift new file mode 100644 index 000000000..273600bd5 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/AccountSettings/OIDCAccountSettingsPresenter.swift @@ -0,0 +1,51 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AuthenticationServices + +/// Presents a web authentication session that will display the user's account settings page. +/// +/// A web authentication session is used so that the same session used for login is available +/// meaning that the user doesn't need to sign in again. `SFSafariViewController` doesn't +/// have access to this session, and for some reason `prefersEphemeralWebBrowserSession` +/// isn't sharing the session back to Safari. +@MainActor +class OIDCAccountSettingsPresenter: NSObject { + private let accountURL: URL + private let presentationAnchor: UIWindow + private let oidcRedirectURL: URL + + init(accountURL: URL, presentationAnchor: UIWindow) { + self.accountURL = accountURL + self.presentationAnchor = presentationAnchor + oidcRedirectURL = ServiceLocator.shared.settings.oidcRedirectURL + super.init() + } + + /// Presents a web authentication session for the supplied data. + func start() { + let session = ASWebAuthenticationSession(url: accountURL, callbackURLScheme: oidcRedirectURL.scheme) { _, _ in } + session.prefersEphemeralWebBrowserSession = false + session.presentationContextProvider = self + session.start() + } +} + +// MARK: ASWebAuthenticationPresentationContextProviding + +extension OIDCAccountSettingsPresenter: ASWebAuthenticationPresentationContextProviding { + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { presentationAnchor } +} diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift index b5d1051da..5c154d30d 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift @@ -48,6 +48,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { switch action { case .close: self.callback?(.dismiss) + case .account: + self.presentAccountSettings() case .analytics: self.presentAnalyticsScreen() case .reportBug: @@ -74,6 +76,26 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { // MARK: - Private + private var accountSettingsPresenter: OIDCAccountSettingsPresenter? + private func presentAccountSettings() { + guard let accountURL = viewModel.context.viewState.accountURL else { + MXLog.error("Account URL is missing.") + return + } + + guard let window = viewModel.context.viewState.window else { + MXLog.error("The window is missing.") + return + } + + // Safari never works in the simulator, use a Web Authentication Session instead. + accountSettingsPresenter = OIDCAccountSettingsPresenter(accountURL: accountURL, presentationAnchor: window) + accountSettingsPresenter?.start() + + // Safari isn't working with the shared browser session 😕 + // UIApplication.shared.open(accountURL) + } + private func presentAnalyticsScreen() { let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics)) diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift index 74096f40f..bf2884665 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift @@ -19,6 +19,7 @@ import UIKit enum SettingsScreenViewModelAction { case close + case account case analytics case reportBug case about @@ -32,11 +33,15 @@ struct SettingsScreenViewState: BindableState { var bindings: SettingsScreenViewStateBindings var deviceID: String? var userID: String + var accountURL: URL? var userAvatarURL: URL? var userDisplayName: String? var showSessionVerificationSection: Bool var showNotificationSettings: Bool var showDeveloperOptions: Bool + + /// The presentation anchor used to display the OIDC account URL. + var window: UIWindow? } struct SettingsScreenViewStateBindings { @@ -45,6 +50,7 @@ struct SettingsScreenViewStateBindings { enum SettingsScreenViewAction { case close + case account case analytics case reportBug case about @@ -53,4 +59,7 @@ enum SettingsScreenViewAction { case changedTimelineStyle case developerOptions case notifications + + /// Updates the window used for the OIDC account URL anchor. + case updateWindow(UIWindow) } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift index cfcb71bec..be81e7bff 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift @@ -39,6 +39,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo super.init(initialViewState: .init(bindings: bindings, deviceID: userSession.deviceID, userID: userSession.userID, + accountURL: userSession.clientProxy.accountURL, showSessionVerificationSection: showSessionVerificationSection, showNotificationSettings: appSettings.notificationSettingsEnabled, showDeveloperOptions: appSettings.canShowDeveloperOptions), @@ -85,6 +86,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo switch viewAction { case .close: callback?(.close) + case .account: + callback?(.account) case .analytics: callback?(.analytics) case .reportBug: @@ -101,6 +104,12 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo callback?(.notifications) case .developerOptions: callback?(.developerOptions) + + case .updateWindow(let window): + Task { + guard state.window != window else { return } + state.window = window + } } } } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift index 3ba232a5e..696616a5f 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift @@ -46,6 +46,9 @@ struct SettingsScreen: View { doneButton } } + .introspect(.window, on: .iOS(.v16)) { window in + context.send(viewAction: .updateWindow(window)) + } } private var versionText: Text { @@ -100,6 +103,17 @@ struct SettingsScreen: View { private var simplifiedSection: some View { Section { + // Account + if context.viewState.accountURL != nil { + ListRow(label: .default(title: L10n.screenSettingsOidcAccount, + systemIcon: .person), + kind: .button { + context.send(viewAction: .account) + }) + .accessibilityIdentifier("notificationsButton") + } + + // Message layout ListRow(label: .default(title: L10n.commonMessageLayout, systemIcon: .rectangleGrid1x2), kind: .picker(selection: $context.timelineStyle, @@ -207,7 +221,8 @@ struct SettingsScreen_Previews: PreviewProvider { verificationController.isVerified = false let userSession = MockUserSession(sessionVerificationController: verificationController, clientProxy: MockClientProxy(userID: "@userid:example.com", - deviceID: "AAAAAAAAAAA"), + deviceID: "AAAAAAAAAAA", + accountURL: "https://matrix.org/account"), mediaProvider: MockMediaProvider()) ServiceLocator.shared.settings.notificationSettingsEnabled = true return SettingsScreenViewModel(userSession: userSession, diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift index 5414a7151..4f6d6825f 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift @@ -31,17 +31,17 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { homeserverSubject = .init(LoginHomeserver(address: appSettings.defaultHomeserverAddress, loginMode: .unknown)) - // let oidcConfiguration = OidcConfiguration(clientName: InfoPlistReader.main.bundleDisplayName, - // redirectUri: settings.oidcRedirectURL.absoluteString, - // clientUri: appSettings.oidcClientURL.absoluteString, - // tosUri: appSettings.oidcTermsURL.absoluteString, - // policyUri: appSettings.oidcPolicyURL.absoluteString, - // staticRegistrations: appSettings.oidcStaticRegistrations.mapKeys { $0.absoluteString }) + let oidcConfiguration = OidcConfiguration(clientName: InfoPlistReader.main.bundleDisplayName, + redirectUri: appSettings.oidcRedirectURL.absoluteString, + clientUri: appSettings.oidcClientURL.absoluteString, + tosUri: appSettings.oidcTermsURL.absoluteString, + policyUri: appSettings.oidcPolicyURL.absoluteString, + staticRegistrations: appSettings.oidcStaticRegistrations.mapKeys { $0.absoluteString }) authenticationService = AuthenticationService(basePath: userSessionStore.baseDirectory.path, passphrase: nil, userAgent: UserAgentBuilder.makeASCIIUserAgent(), - // oidcConfiguration: oidcConfiguration, + oidcConfiguration: oidcConfiguration, customSlidingSyncProxy: appSettings.slidingSyncProxyURL?.absoluteString) } @@ -56,7 +56,7 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { } if let details = authenticationService.homeserverDetails() { - if details.authenticationIssuer() != nil { + if details.supportsOidcLogin() { homeserver.loginMode = .oidc } else if details.supportsPasswordLogin() { homeserver.loginMode = .password @@ -77,31 +77,29 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { } func urlForOIDCLogin() async -> Result { - .failure(.oidcError(.notSupported)) -// do { -// let oidcData = try await Task.dispatch(on: .global()) { -// try self.authenticationService.urlForOidcLogin() -// } -// return .success(OIDCAuthenticationDataProxy(underlyingData: oidcData)) -// } catch { -// MXLog.error("Failed to get URL for OIDC login: \(error)") -// return .failure(.oidcError(.urlFailure)) -// } + do { + let oidcData = try await Task.dispatch(on: .global()) { + try self.authenticationService.urlForOidcLogin() + } + return .success(OIDCAuthenticationDataProxy(underlyingData: oidcData)) + } catch { + MXLog.error("Failed to get URL for OIDC login: \(error)") + return .failure(.oidcError(.urlFailure)) + } } func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthenticationDataProxy) async -> Result { - .failure(.oidcError(.notSupported)) -// do { -// let client = try await Task.dispatch(on: .global()) { -// try self.authenticationService.loginWithOidcCallback(authenticationData: data.underlyingData, callbackUrl: callbackURL.absoluteString) -// } -// return await userSession(for: client) -// } catch AuthenticationError.OidcCancelled { -// return .failure(.oidcError(.userCancellation)) -// } catch { -// MXLog.error("Login with OIDC failed: \(error)") -// return .failure(.failedLoggingIn) -// } + do { + let client = try await Task.dispatch(on: .global()) { + try self.authenticationService.loginWithOidcCallback(authenticationData: data.underlyingData, callbackUrl: callbackURL.absoluteString) + } + return await userSession(for: client) + } catch AuthenticationError.OidcCancelled { + return .failure(.oidcError(.userCancellation)) + } catch { + MXLog.error("Login with OIDC failed: \(error)") + return .failure(.failedLoggingIn) + } } func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result { diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift index 0cd735741..c8fc7ce39 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift @@ -66,16 +66,18 @@ enum OIDCError: Error { } struct OIDCAuthenticationDataProxy: Equatable { -// let underlyingData: OidcAuthenticationData -// -// var url: URL { -// URL(string: underlyingData.loginUrl())! -// } - let url: URL = "https://theroadtonowhere" + let underlyingData: OidcAuthenticationData + + var url: URL { + guard let url = URL(string: underlyingData.loginUrl()) else { + fatalError("OIDC login URL hasn't been validated.") + } + return url + } } -// extension OidcAuthenticationData: Equatable { -// public static func == (lhs: MatrixRustSDK.OidcAuthenticationData, rhs: MatrixRustSDK.OidcAuthenticationData) -> Bool { -// lhs.loginUrl() == rhs.loginUrl() -// } -// } +extension OidcAuthenticationData: Equatable { + public static func == (lhs: MatrixRustSDK.OidcAuthenticationData, rhs: MatrixRustSDK.OidcAuthenticationData) -> Bool { + lhs.loginUrl() == rhs.loginUrl() + } +} diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 933e4dd3d..7363d300f 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -108,6 +108,10 @@ class ClientProxy: ClientProxyProtocol { return nil } } + + var accountURL: URL? { + client.accountUrl().flatMap(URL.init(string:)) + } func startSync() { MXLog.info("Starting sync") @@ -305,7 +309,8 @@ class ClientProxy: ClientProxyProtocol { func logout() async { await Task.dispatch(on: clientQueue) { do { - try self.client.logout() + // We aren't currently handling the RP initiated sign out URL. + _ = try self.client.logout() } catch { MXLog.error("Failed logging out with error: \(error)") } diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 0786f92df..1e5ba50f6 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -76,6 +76,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { var restorationToken: RestorationToken? { get } + var accountURL: URL? { get } + var roomSummaryProvider: RoomSummaryProviderProtocol? { get } var inviteSummaryProvider: RoomSummaryProviderProtocol? { get } diff --git a/ElementX/Sources/Services/Client/MockClientProxy.swift b/ElementX/Sources/Services/Client/MockClientProxy.swift index 36eb461b6..9a38def36 100644 --- a/ElementX/Sources/Services/Client/MockClientProxy.swift +++ b/ElementX/Sources/Services/Client/MockClientProxy.swift @@ -25,6 +25,7 @@ class MockClientProxy: ClientProxyProtocol { let deviceID: String? let homeserver = "" let restorationToken: RestorationToken? = nil + let accountURL: URL? var roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider() @@ -32,9 +33,10 @@ class MockClientProxy: ClientProxyProtocol { var avatarURLPublisher: AnyPublisher { Empty().eraseToAnyPublisher() } - init(userID: String, deviceID: String? = nil, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) { + init(userID: String, deviceID: String? = nil, accountURL: URL? = nil, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) { self.userID = userID self.deviceID = deviceID + self.accountURL = accountURL self.roomSummaryProvider = roomSummaryProvider } diff --git a/ElementX/Sources/Services/UserSession/RestorationToken.swift b/ElementX/Sources/Services/UserSession/RestorationToken.swift index 38bbe8405..4c5bf28fd 100644 --- a/ElementX/Sources/Services/UserSession/RestorationToken.swift +++ b/ElementX/Sources/Services/UserSession/RestorationToken.swift @@ -42,8 +42,8 @@ extension MatrixRustSDK.Session: Codable { userId: container.decode(String.self, forKey: .userId), deviceId: container.decode(String.self, forKey: .deviceId), homeserverUrl: container.decode(String.self, forKey: .homeserverUrl), + oidcData: container.decodeIfPresent(String.self, forKey: .oidcData), slidingSyncProxy: container.decode(String.self, forKey: .slidingSyncProxy)) - // oidcData: container.decodeIfPresent(String.self, forKey: .oidcData) } public func encode(to encoder: Encoder) throws { @@ -53,12 +53,11 @@ extension MatrixRustSDK.Session: Codable { try container.encode(userId, forKey: .userId) try container.encode(deviceId, forKey: .deviceId) try container.encode(homeserverUrl, forKey: .homeserverUrl) + try container.encode(oidcData, forKey: .oidcData) try container.encode(slidingSyncProxy, forKey: .slidingSyncProxy) - // try container.encode(oidcData, forKey: .oidcData) } enum CodingKeys: String, CodingKey { - case accessToken, refreshToken, userId, deviceId, homeserverUrl, slidingSyncProxy - // case oidcData + case accessToken, refreshToken, userId, deviceId, homeserverUrl, oidcData, slidingSyncProxy } } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 90c4a2133..2f95be6e7 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -166,10 +166,10 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .settings: let navigationStackCoordinator = NavigationStackCoordinator() + let clientProxy = MockClientProxy(userID: "@mock:client.com", accountURL: "https://matrix.org/account") let coordinator = SettingsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, userIndicatorController: nil, - userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"), - mediaProvider: MockMediaProvider()), + userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()), bugReportService: BugReportServiceMock(), notificationSettings: NotificationSettingsProxyMock(with: .init()))) navigationStackCoordinator.setRootCoordinator(coordinator) diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index 6b53faa17..3e283b66b 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -40,6 +40,12 @@ class NotificationServiceExtension: UNNotificationServiceExtension { // - NotificationID could not be resolved return contentHandler(request.content) } + + if credentials.restorationToken.session.oidcData != nil { + // Notification content is disabled for OIDC sessions + // until token refresh is multi-process aware. + return contentHandler(request.content) + } handler = contentHandler modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png index 4ac0e2e84..e051ea9b8 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ce135e012a85e14fdbf8ad7a62d002d64b1b64b43be51cccc64c75793c7601f -size 113300 +oid sha256:cc23b1fe8559fd145298edd227effe614754d793c5109f2a9e7e19e0c1747a6e +size 120126 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png index aec05a2f5..19eb3b170 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bd929af18ca9f589d6cc6a2c9448708b62b70a922cb9e5ffe1e20a4fed1b672 -size 140569 +oid sha256:c6d8a57c11b416900758ac53ebf2e4b5e27a8789d271d6a01bcf2e085dc7c0e8 +size 150622 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png index b1ce9e0d5..c97fa97c2 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4efcb97579d3cfc8140578e9c5d0a2945435013c16ad2cfff3b1de970608bd50 -size 120591 +oid sha256:27ecfa7b86dd07ebf9b8b9ad4fd1faf13e15ec2165a90d5e842706291f89d8d6 +size 127331 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png index d3abe4259..4868841d7 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e014fead3ae053429a182e79b1cf3951323977d602f4c45667cb5ba2ba3057a -size 165296 +oid sha256:01edf9cb7adc62458ef5f9fc02bf6bd283147269879b1c471f3eadc95343e6ff +size 177926 diff --git a/UnitTests/Sources/KeychainControllerTests.swift b/UnitTests/Sources/KeychainControllerTests.swift index d4fac3d84..9897d7c27 100644 --- a/UnitTests/Sources/KeychainControllerTests.swift +++ b/UnitTests/Sources/KeychainControllerTests.swift @@ -37,6 +37,7 @@ class KeychainControllerTests: XCTestCase { userId: "userId", deviceId: "deviceId", homeserverUrl: "homeserverUrl", + oidcData: "oidcData", slidingSyncProxy: "https://my.sync.proxy")) keychain.setRestorationToken(restorationToken, forUsername: username) @@ -52,6 +53,7 @@ class KeychainControllerTests: XCTestCase { userId: "userId", deviceId: "deviceId", homeserverUrl: "homeserverUrl", + oidcData: "oidcData", slidingSyncProxy: "https://my.sync.proxy")) keychain.setRestorationToken(restorationToken, forUsername: username) XCTAssertEqual(keychain.restorationTokens().count, 1, "The keychain should have 1 restoration token.") @@ -73,6 +75,7 @@ class KeychainControllerTests: XCTestCase { userId: "userId", deviceId: "deviceId", homeserverUrl: "homeserverUrl", + oidcData: "oidcData", slidingSyncProxy: "https://my.sync.proxy")) keychain.setRestorationToken(restorationToken, forUsername: "@test\(index):example.com") } @@ -93,6 +96,7 @@ class KeychainControllerTests: XCTestCase { userId: "userId", deviceId: "deviceId", homeserverUrl: "homeserverUrl", + oidcData: "oidcData", slidingSyncProxy: "https://my.sync.proxy")) keychain.setRestorationToken(restorationToken, forUsername: "@test\(index):example.com") } diff --git a/changelog.d/261.feature b/changelog.d/261.feature new file mode 100644 index 000000000..4a4f567e7 --- /dev/null +++ b/changelog.d/261.feature @@ -0,0 +1 @@ +Enable OIDC support, with notification content disabled for now. \ No newline at end of file diff --git a/project.yml b/project.yml index 9acb9e6d7..447bdb2b8 100644 --- a/project.yml +++ b/project.yml @@ -45,7 +45,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/matrix-org/matrix-rust-components-swift - exactVersion: 1.0.110-alpha + exactVersion: 1.0.112-alpha # path: ../matrix-rust-sdk DesignKit: path: DesignKit