diff --git a/Podfile.lock b/Podfile.lock index 8bab1603c..709c73cdd 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -183,7 +183,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: AnalyticsEvents: - :commit: f37a2f243270bffdcddfa5cbaeb4379e0db581c2 + :commit: b275ccb194a219a61b3100159d51cadbf7c9020c :git: https://github.com/matrix-org/matrix-analytics-events.git SPEC CHECKSUMS: diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index 39a4ffb54..9505b5620 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -226,6 +226,13 @@ extension Analytics { client.updateUserProperties(userProperties) } + /// Track the registration of a new user. + /// - Parameter authenticationType: The type of authentication that was used. + func trackSignup(authenticationType: AnalyticsEvent.Signup.AuthenticationType) { + let event = AnalyticsEvent.Signup(authenticationType: authenticationType) + capture(event: event) + } + /// Track the presentation of a screen /// - Parameters: /// - screen: The screen that was shown. diff --git a/Riot/Modules/Analytics/AnalyticsJoinedRoomTrigger.swift b/Riot/Modules/Analytics/AnalyticsJoinedRoomTrigger.swift index 2b830870c..aecfef406 100644 --- a/Riot/Modules/Analytics/AnalyticsJoinedRoomTrigger.swift +++ b/Riot/Modules/Analytics/AnalyticsJoinedRoomTrigger.swift @@ -26,6 +26,7 @@ import AnalyticsEvents case slashCommand case spaceHierarchy case timeline + case permalink var trigger: AnalyticsEvent.JoinedRoom.Trigger? { switch self { @@ -45,6 +46,8 @@ import AnalyticsEvents return .SpaceHierarchy case .timeline: return .Timeline + case .permalink: + return .MobilePermalink } } } diff --git a/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift b/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift index 1be27326a..65c6b4429 100644 --- a/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift +++ b/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift @@ -39,6 +39,11 @@ import AnalyticsEvents case inCall case spaceMenu case spaceSettings + case roomPreview + case permalink + case linkShare + case exploreRooms + case spaceMembers var trigger: AnalyticsEvent.ViewRoom.Trigger? { switch self { @@ -84,6 +89,16 @@ import AnalyticsEvents return .MobileSpaceMenu case .spaceSettings: return .MobileSpaceSettings + case .roomPreview: + return .MobileRoomPreview + case .permalink: + return .MobilePermalink + case .linkShare: + return .MobileLinkShare + case .exploreRooms: + return .MobileExploreRooms + case .spaceMembers: + return .MobileSpaceMembers } } } diff --git a/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift b/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift index 59db49bb1..196c2168f 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift @@ -21,7 +21,7 @@ import Foundation enum AuthenticationCoordinatorResult { /// The user has authenticated but key verification is yet to happen. The session value is /// for a fresh session that still needs to load, sync etc before being ready. - case didLogin(session: MXSession, authenticationType: MXKAuthenticationType) + case didLogin(session: MXSession, authenticationFlow: AuthenticationFlow, authenticationType: AuthenticationType) /// All of the required authentication steps including key verification is complete. case didComplete } @@ -34,7 +34,7 @@ protocol AuthenticationCoordinatorProtocol: Coordinator, Presentable { var customServerFieldsVisible: Bool { get set } /// Update the screen to display registration or login. - func update(authenticationType: MXKAuthenticationType) + func update(authenticationFlow: AuthenticationFlow) /// Force a registration process based on a predefined set of parameters from a server provisioning link. /// For more information see `AuthenticationViewController.externalRegistrationParameters`. diff --git a/Riot/Modules/Authentication/AuthenticationViewController.h b/Riot/Modules/Authentication/AuthenticationViewController.h index f023490c0..130d94ec4 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.h +++ b/Riot/Modules/Authentication/AuthenticationViewController.h @@ -19,6 +19,7 @@ #import "MatrixKit.h" @protocol AuthenticationViewControllerDelegate; +@class SSOIdentityProvider; @interface AuthenticationViewController : MXKAuthenticationViewController @@ -60,8 +61,16 @@ @protocol AuthenticationViewControllerDelegate -- (void)authenticationViewController:(AuthenticationViewController *)authenticationViewController +/** + Notifies the delegate that authentication has succeeded. + @param authenticationViewController The view controller that handled the authentication. + @param session The session for the authenticated account. + @param password Optional password used for authentication (to be handed to the verification flow). + @param identityProvider Optional SSO identity provider used for authentication. + */ +- (void)authenticationViewController:(AuthenticationViewController * _Nonnull)authenticationViewController didLoginWithSession:(MXSession *)session - andPassword:(NSString *)password; + andPassword:(NSString * _Nullable)password + orSSOIdentityProvider:(SSOIdentityProvider * _Nullable)identityProvider; @end; diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m index ff5c06ca8..1f26b020a 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -76,6 +76,10 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; // Current SSO transaction id used to identify and validate the SSO authentication callback @property (nonatomic, strong) NSString *ssoCallbackTxnId; +/** + The SSO provider that was used to successfully complete login, otherwise `nil`. + */ +@property (nonatomic, readwrite, nullable) SSOIdentityProvider *ssoIdentityProvider; @property (nonatomic, getter = isFirstViewAppearing) BOOL firstViewAppearing; @@ -1351,7 +1355,10 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; } // Ask the coordinator to show the loading spinner whilst waiting. - [self.authVCDelegate authenticationViewController:self didLoginWithSession:session andPassword:self.authInputsView.password]; + [self.authVCDelegate authenticationViewController:self + didLoginWithSession:session + andPassword:self.authInputsView.password + orSSOIdentityProvider:self.ssoIdentityProvider]; } #pragma mark - MXKAuthInputsViewDelegate @@ -1590,14 +1597,14 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; #pragma mark - SocialLoginListViewDelegate -- (void)socialLoginListView:(SocialLoginListView *)socialLoginListView didTapSocialButtonWithIdentifier:(NSString *)identifier +- (void)socialLoginListView:(SocialLoginListView *)socialLoginListView didTapSocialButtonWithProvider:(SSOIdentityProvider *)identityProvider { - [self presentSSOAuthenticationForIdentityProviderIdentifier:identifier]; + [self presentSSOAuthenticationForIdentityProvider:identityProvider]; } #pragma mark - SSOIdentityProviderAuthenticationPresenter -- (void)presentSSOAuthenticationForIdentityProviderIdentifier:(NSString*)identityProviderIdentifier +- (void)presentSSOAuthenticationForIdentityProvider:(SSOIdentityProvider*)identityProvider { NSString *homeServerStringURL = self.homeServerTextField.text; @@ -1615,7 +1622,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; // Generate a unique identifier that will identify the success callback URL NSString *transactionId = [MXTools generateTransactionId]; - [presenter presentForIdentityProviderIdentifier:identityProviderIdentifier with: transactionId from:self animated:YES]; + [presenter presentForIdentityProvider:identityProvider with: transactionId from:self animated:YES]; self.ssoCallbackTxnId = transactionId; self.ssoAuthenticationPresenter = presenter; @@ -1623,7 +1630,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; - (void)presentDefaultSSOAuthentication { - [self presentSSOAuthenticationForIdentityProviderIdentifier:nil]; + [self presentSSOAuthenticationForIdentityProvider:nil]; } - (void)dismissSSOAuthenticationPresenter @@ -1656,8 +1663,11 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; [self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil]; } -- (void)ssoAuthenticationPresenter:(SSOAuthenticationPresenter *)presenter authenticationSucceededWithToken:(NSString *)token +- (void)ssoAuthenticationPresenter:(SSOAuthenticationPresenter *)presenter + authenticationSucceededWithToken:(NSString *)token + usingIdentityProvider:(SSOIdentityProvider * _Nullable)identityProvider { + self.ssoIdentityProvider = identityProvider; [self dismissSSOAuthenticationPresenter]; [self loginWithToken:token]; } diff --git a/Riot/Modules/Authentication/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/LegacyAuthenticationCoordinator.swift index 8c546c8c5..e6c3523f5 100644 --- a/Riot/Modules/Authentication/LegacyAuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/LegacyAuthenticationCoordinator.swift @@ -81,8 +81,8 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator return self.authenticationViewController } - func update(authenticationType: MXKAuthenticationType) { - authenticationViewController.authType = authenticationType + func update(authenticationFlow: AuthenticationFlow) { + authenticationViewController.authType = authenticationFlow.mxkType } func update(externalRegistrationParameters: [AnyHashable: Any]) { @@ -149,7 +149,16 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator // MARK: - AuthenticationViewControllerDelegate extension LegacyAuthenticationCoordinator: AuthenticationViewControllerDelegate { - func authenticationViewController(_ authenticationViewController: AuthenticationViewController!, didLoginWith session: MXSession!, andPassword password: String!) { + func authenticationViewController(_ authenticationViewController: AuthenticationViewController, + didLoginWith session: MXSession!, + andPassword password: String?, + orSSOIdentityProvider identityProvider: SSOIdentityProvider?) { + // Sanity check + guard let session = session else { + MXLog.failure("[LegacyAuthenticationCoordinator] authenticationViewController(_:didLoginWith:) The MXSession should not be nil.") + return + } + self.session = session if canPresentAdditionalScreens { @@ -177,8 +186,18 @@ extension LegacyAuthenticationCoordinator: AuthenticationViewControllerDelegate verificationListener.start() self.verificationListener = verificationListener - - completion?(.didLogin(session: session, authenticationType: authenticationViewController.authType)) + let authenticationType: AuthenticationType + if let identityProvider = identityProvider { + authenticationType = .sso(identityProvider) + } else if !password.isEmptyOrNil { + authenticationType = .password + } else { + authenticationType = .other + } + + completion?(.didLogin(session: session, + authenticationFlow: authenticationViewController.authType.flow, + authenticationType: authenticationType)) } } @@ -210,3 +229,29 @@ extension LegacyAuthenticationCoordinator: UIAdaptivePresentationControllerDeleg return false } } + + +fileprivate extension AuthenticationFlow { + var mxkType: MXKAuthenticationType { + switch self { + case .login: + return .login + case .register: + return .register + } + } +} + +fileprivate extension MXKAuthenticationType { + var flow: AuthenticationFlow { + switch self { + case .register: + return .register + case .login, .forgotPassword: + return .login + @unknown default: + MXLog.failure("[MXKAuthenticationType] Unknown type exposed to Swift.") + return .login + } + } +} diff --git a/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift b/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift index 182d1c74b..2f7c0e1a0 100644 --- a/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift +++ b/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift @@ -20,7 +20,9 @@ import SafariServices @objc protocol SSOAuthenticationPresenterDelegate { func ssoAuthenticationPresenterDidCancel(_ presenter: SSOAuthenticationPresenter) func ssoAuthenticationPresenter(_ presenter: SSOAuthenticationPresenter, authenticationDidFailWithError error: Error) - func ssoAuthenticationPresenter(_ presenter: SSOAuthenticationPresenter, authenticationSucceededWithToken token: String) + func ssoAuthenticationPresenter(_ presenter: SSOAuthenticationPresenter, + authenticationSucceededWithToken token: String, + usingIdentityProvider identityProvider: SSOIdentityProvider?) } enum SSOAuthenticationPresenterError: Error { @@ -46,6 +48,7 @@ final class SSOAuthenticationPresenter: NSObject { // MARK: Public + private(set) var identityProvider: SSOIdentityProvider? weak var delegate: SSOAuthenticationPresenterDelegate? // MARK: - Setup @@ -57,15 +60,16 @@ final class SSOAuthenticationPresenter: NSObject { // MARK: - Public - func present(forIdentityProviderIdentifier identityProviderIdentifier: String?, + func present(forIdentityProvider identityProvider: SSOIdentityProvider?, with transactionId: String, from presentingViewController: UIViewController, animated: Bool) { - guard let authenticationURL = self.ssoAuthenticationService.authenticationURL(for: identityProviderIdentifier, transactionId: transactionId) else { + guard let authenticationURL = self.ssoAuthenticationService.authenticationURL(for: identityProvider?.id, transactionId: transactionId) else { self.delegate?.ssoAuthenticationPresenter(self, authenticationDidFailWithError: SSOAuthenticationPresenterError.failToLoadAuthenticationURL) return } + self.identityProvider = identityProvider self.presentingViewController = presentingViewController // NOTE: By using SFAuthenticationSession the consent alert show product name instead of display name. Fallback to SFSafariViewController instead in order to not disturb users with "Riot" wording at the moment. @@ -130,7 +134,7 @@ final class SSOAuthenticationPresenter: NSObject { } } else if let successURL = callBackURL { if let loginToken = self.ssoAuthenticationService.loginToken(from: successURL) { - self.delegate?.ssoAuthenticationPresenter(self, authenticationSucceededWithToken: loginToken) + self.delegate?.ssoAuthenticationPresenter(self, authenticationSucceededWithToken: loginToken, usingIdentityProvider: self.identityProvider) } else { MXLog.debug("SSOAuthenticationPresenter: Login token not found") self.delegate?.ssoAuthenticationPresenter(self, authenticationDidFailWithError: SSOAuthenticationServiceError.tokenNotFound) diff --git a/Riot/Modules/Authentication/SocialLogin/SocialLoginButton.swift b/Riot/Modules/Authentication/SocialLogin/SocialLoginButton.swift index 9fc40edaf..0f5aef8e0 100644 --- a/Riot/Modules/Authentication/SocialLogin/SocialLoginButton.swift +++ b/Riot/Modules/Authentication/SocialLogin/SocialLoginButton.swift @@ -42,7 +42,10 @@ final class SocialLoginButton: UIButton, Themable { // MARK: Public var identifier: String? { - return self.viewData?.identifier + viewData?.identityProvider.id + } + var identityProvider: SSOIdentityProvider? { + viewData?.identityProvider } // MARK: Setup diff --git a/Riot/Modules/Authentication/SocialLogin/SocialLoginButtonFactory.swift b/Riot/Modules/Authentication/SocialLogin/SocialLoginButtonFactory.swift index 49a5f7ccc..21a7c5d29 100644 --- a/Riot/Modules/Authentication/SocialLogin/SocialLoginButtonFactory.swift +++ b/Riot/Modules/Authentication/SocialLogin/SocialLoginButtonFactory.swift @@ -71,7 +71,7 @@ class SocialLoginButtonFactory { let title = self.buildButtonTitle(with: identityProvider.name, mode: mode) - let viewData = SocialLoginButtonViewData(identifier: identityProvider.identifier, + let viewData = SocialLoginButtonViewData(identityProvider: identityProvider.ssoIdentityProvider, title: title, defaultStyle: defaultStyle, themeStyles: styles) diff --git a/Riot/Modules/Authentication/SocialLogin/SocialLoginButtonViewData.swift b/Riot/Modules/Authentication/SocialLogin/SocialLoginButtonViewData.swift index 568479ea4..5de0bd748 100644 --- a/Riot/Modules/Authentication/SocialLogin/SocialLoginButtonViewData.swift +++ b/Riot/Modules/Authentication/SocialLogin/SocialLoginButtonViewData.swift @@ -19,8 +19,8 @@ import Foundation /// SocialLoginButton view data struct SocialLoginButtonViewData { - /// Identify provider identifier - let identifier: String + /// Identity provider identifier + let identityProvider: SSOIdentityProvider /// Button title let title: String diff --git a/Riot/Modules/Authentication/SocialLogin/SocialLoginListView.swift b/Riot/Modules/Authentication/SocialLogin/SocialLoginListView.swift index e2e1a586f..dc80526b6 100644 --- a/Riot/Modules/Authentication/SocialLogin/SocialLoginListView.swift +++ b/Riot/Modules/Authentication/SocialLogin/SocialLoginListView.swift @@ -18,7 +18,7 @@ import UIKit import Reusable @objc protocol SocialLoginListViewDelegate: AnyObject { - func socialLoginListView(_ socialLoginListView: SocialLoginListView, didTapSocialButtonWithIdentifier identifier: String) + func socialLoginListView(_ socialLoginListView: SocialLoginListView, didTapSocialButtonWithProvider identityProvider: SSOIdentityProvider) } /// SocialLoginListView displays a list of social login buttons according to a given array of SSO Identity Providers. @@ -139,10 +139,10 @@ final class SocialLoginListView: UIView, NibLoadable { // MARK: - Action @objc private func socialButtonAction(_ socialLoginButton: SocialLoginButton) { - guard let identifier = socialLoginButton.identifier else { + guard let provider = socialLoginButton.identityProvider else { return } - self.delegate?.socialLoginListView(self, didTapSocialButtonWithIdentifier: identifier) + self.delegate?.socialLoginListView(self, didTapSocialButtonWithProvider: provider) } } diff --git a/Riot/Modules/Onboarding/AuthenticationCoordinator.swift b/Riot/Modules/Onboarding/AuthenticationCoordinator.swift index bd2409670..2c76eee80 100644 --- a/Riot/Modules/Onboarding/AuthenticationCoordinator.swift +++ b/Riot/Modules/Onboarding/AuthenticationCoordinator.swift @@ -76,7 +76,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc func start() { Task { do { - let flow: AuthenticationFlow = initialScreen == .login ? .login : .registration + let flow: AuthenticationFlow = initialScreen == .login ? .login : .register try await authenticationService.startFlow(flow, for: authenticationService.state.homeserver.address) } catch { MXLog.error("[AuthenticationCoordinator] start: Failed to start") @@ -211,7 +211,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc func handleRegistrationResult(_ result: RegistrationResult) { switch result { case .success(let mxSession): - onSessionCreated(session: mxSession, isAccountCreated: true) + onSessionCreated(session: mxSession, flow: .register) case .flowResponse(let flowResult): // TODO break @@ -219,7 +219,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc } /// Handles the creation of a new session following on from a successful authentication. - func onSessionCreated(session: MXSession, isAccountCreated: Bool) { + func onSessionCreated(session: MXSession, flow: AuthenticationFlow) { self.session = session // self.password = password @@ -249,7 +249,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc verificationListener.start() self.verificationListener = verificationListener - completion?(.didLogin(session: session, authenticationType: isAccountCreated ? .register : .login)) + #warning("Add authentication type to the new flow") + completion?(.didLogin(session: session, authenticationFlow: flow, authenticationType: .other)) } // MARK: - Additional Screens @@ -331,7 +332,7 @@ extension AuthenticationCoordinator { set { /* no-op */ } } - func update(authenticationType: MXKAuthenticationType) { + func update(authenticationFlow: AuthenticationFlow) { // unused } diff --git a/Riot/Modules/Onboarding/AuthenticationType+Analytics.swift b/Riot/Modules/Onboarding/AuthenticationType+Analytics.swift new file mode 100644 index 000000000..c54cca2e7 --- /dev/null +++ b/Riot/Modules/Onboarding/AuthenticationType+Analytics.swift @@ -0,0 +1,45 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AnalyticsEvents + +extension AuthenticationType { + var analyticsType: AnalyticsEvent.Signup.AuthenticationType { + switch self { + case .password: + return .Password + case .sso(let provider): + guard let brandString = provider.brand else { return .SSO } + let brand = MXLoginSSOIdentityProviderBrand(brandString) + switch brand { + case .apple: + return .Apple + case .facebook: + return .Facebook + case .github: + return .GitHub + case .gitlab: + return .GitLab + case .google: + return .Google + default: + return .SSO + } + case .other: + return .Other + } + } +} diff --git a/Riot/Modules/Onboarding/OnboardingCoordinator.swift b/Riot/Modules/Onboarding/OnboardingCoordinator.swift index 0d3d68906..14b96ac0f 100644 --- a/Riot/Modules/Onboarding/OnboardingCoordinator.swift +++ b/Riot/Modules/Onboarding/OnboardingCoordinator.swift @@ -63,7 +63,10 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { // MARK: Screen results private var splashScreenResult: OnboardingSplashScreenViewModelResult? private var useCaseResult: OnboardingUseCaseViewModelResult? - private var authenticationType: MXKAuthenticationType? + /// The flow being used for authentication. + private var authenticationFlow: AuthenticationFlow? + /// The type of authentication used to login/register. + private var authenticationType: AuthenticationType? private var session: MXSession? /// A place to store the image selected in the avatar screen until it has been saved. private var selectedAvatar: UIImage? @@ -154,7 +157,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { splashScreenResult = result // Set the auth type early to allow network requests to finish during display of the use case screen. - authenticationCoordinator.update(authenticationType: result.mxkAuthenticationType) + authenticationCoordinator.update(authenticationFlow: result.flow) switch result { case .register: @@ -219,8 +222,8 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { guard let self = self, let coordinator = coordinator else { return } switch result { - case .didLogin(let session, let authenticationType): - self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationType) + case .didLogin(let session, let authenticationFlow, let authenticationType): + self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationFlow, using: authenticationType) case .didComplete: self.authenticationCoordinatorDidComplete(coordinator) } @@ -241,8 +244,8 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { guard let self = self, let coordinator = coordinator else { return } switch result { - case .didLogin(let session, let authenticationType): - self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationType) + case .didLogin(let session, let authenticationFlow, let authenticationType): + self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationFlow, using: authenticationType) case .didComplete: self.authenticationCoordinatorDidComplete(coordinator) } @@ -284,13 +287,15 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { /// whilst crypto and the rest of the app is launching in the background. private func authenticationCoordinator(_ coordinator: AuthenticationCoordinatorProtocol, didLoginWith session: MXSession, - and authenticationType: MXKAuthenticationType) { + and authenticationFlow: AuthenticationFlow, + using authenticationType: AuthenticationType) { self.session = session + self.authenticationFlow = authenticationFlow self.authenticationType = authenticationType // Check whether another screen should be shown. if #available(iOS 14.0, *) { - if authenticationType == .register, + if authenticationFlow == .register, let userId = session.credentials.userId, let userSession = UserSessionsService.shared.userSession(withUserId: userId) { // If personalisation is to be shown, check that the homeserver supports it otherwise show the congratulations screen @@ -335,7 +340,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { isShowingLegacyAuthentication = false // Handle the chosen use case where applicable - if authenticationType == .register, + if authenticationFlow == .register, let useCase = useCaseResult?.userSessionPropertyValue, let userSession = UserSessionsService.shared.mainUserSession { // Store the value in the user's session @@ -559,15 +564,28 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { return } + trackSignup() + completion?() } + + /// Sends a signup event to the Analytics class if onboarding has completed via the register flow. + private func trackSignup() { + guard authenticationFlow == .register else { return } + guard let authenticationType = authenticationType else { + MXLog.warning("[OnboardingCoordinator] sendSignedEvent: Registration finished without collecting an authentication type.") + return + } + + Analytics.shared.trackSignup(authenticationType: authenticationType.analyticsType) + } } // MARK: - Helpers extension OnboardingSplashScreenViewModelResult { - /// The result converted into the MatrixKit authentication type to use. - var mxkAuthenticationType: MXKAuthenticationType { + /// The result converted into an authentication flow. + var flow: AuthenticationFlow { switch self { case .login: return .login diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift index 40f59eb50..a6bba7b80 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift @@ -32,6 +32,7 @@ class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, Room } locationView.update(theme: ThemeService.shared().theme) + locationView.delegate = self if bubbleData.cellDataTag == .location, let event = bubbleData.events.last { @@ -130,18 +131,10 @@ class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, Room extension LocationPlainCell: RoomTimelineLocationViewDelegate { func roomTimelineLocationViewDidTapStopButton(_ roomTimelineLocationView: RoomTimelineLocationView) { - guard let event = self.event else { - return - } - - delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellStopShareButtonPressed, userInfo: [kMXKRoomBubbleCellEventKey: event]) + delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellStopShareButtonPressed, userInfo: nil) } func roomTimelineLocationViewDidTapRetryButton(_ roomTimelineLocationView: RoomTimelineLocationView) { - guard let event = self.event else { - return - } - - delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellRetryShareButtonPressed, userInfo: [kMXKRoomBubbleCellEventKey: event]) + delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellRetryShareButtonPressed, userInfo: nil) } } diff --git a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift index 75fcac428..d68ac7248 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift @@ -16,10 +16,20 @@ import Foundation -/// A value that represents the type of authentication flow being used. +/// A value that represents an authentication flow as either login or register. enum AuthenticationFlow { case login - case registration + case register +} + +/// A value that represents the type of authentication used. +enum AuthenticationType { + /// A username and password. + case password + /// SSO with the associated provider + case sso(SSOIdentityProvider) + /// Some other method such as the fall back page. + case other } /// Errors that can be thrown from `AuthenticationService`. @@ -65,8 +75,8 @@ enum LoginError: String, Error { } /// Represents an SSO Identity Provider as provided in a login flow. -struct SSOIdentityProvider: Identifiable { - /// The identifier field (id field in JSON) is the Identity Provider identifier used for the SSO Web page redirection `/login/sso/redirect/{idp_id}`. +@objc class SSOIdentityProvider: NSObject, Identifiable { + /// The id field is the Identity Provider identifier used for the SSO Web page redirection `/login/sso/redirect/{idp_id}`. let id: String /// The name field is a human readable string intended to be printed by the client. let name: String @@ -74,4 +84,11 @@ struct SSOIdentityProvider: Identifiable { let brand: String? /// The icon field is an optional field that points to an icon representing the identity provider. If present then it must be an HTTPS URL to an image resource. let iconURL: String? + + init(id: String, name: String, brand: String?, iconURL: String?) { + self.id = id + self.name = name + self.brand = brand + self.iconURL = iconURL + } } diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift index 38021f43e..da6658b63 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift @@ -98,7 +98,7 @@ class AuthenticationService: NSObject { let loginWizard = LoginWizard() self.loginWizard = loginWizard - if flow == .registration { + if flow == .register { do { let registrationWizard = RegistrationWizard(client: client) state.homeserver.registrationFlow = try await registrationWizard.registrationFlow() @@ -124,7 +124,7 @@ class AuthenticationService: NSObject { switch flow { case .login: return client.loginFallbackURL - case .registration: + case .register: return client.registerFallbackURL } } diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift index 6b9401cc5..9af3a728c 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift @@ -119,7 +119,7 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable { Task { do { #warning("The screen should be configuration for .login too.") - try await authenticationService.startFlow(.registration, for: homeserverAddress) + try await authenticationService.startFlow(.register, for: homeserverAddress) stopLoading() completion?(.updated) diff --git a/RiotTests/AuthenticationServiceTests.swift b/RiotTests/AuthenticationServiceTests.swift index c5f520c76..c76c4022a 100644 --- a/RiotTests/AuthenticationServiceTests.swift +++ b/RiotTests/AuthenticationServiceTests.swift @@ -39,7 +39,7 @@ class AuthenticationServiceTests: XCTestCase { XCTAssertNil(service.state.homeserver.registrationFlow, "A new service shouldn't provide a registration flow for the homeserver.") // When starting a new registration flow. - try await service.startFlow(.registration, for: "https://matrix.org") + try await service.startFlow(.register, for: "https://matrix.org") // Then a registration wizard should be available for use. XCTAssertNotNil(service.registrationWizard, "The registration wizard should exist after starting a registration flow.") @@ -49,13 +49,13 @@ class AuthenticationServiceTests: XCTestCase { func testReset() async throws { // Given a service that has begun registration. let service = AuthenticationService() - try await service.startFlow(.registration, for: "https://matrix.org") + try await service.startFlow(.register, for: "https://matrix.org") _ = try await service.registrationWizard?.createAccount(username: UUID().uuidString, password: UUID().uuidString, initialDeviceDisplayName: "Test") XCTAssertNotNil(service.loginWizard, "The login wizard should exist after starting a registration flow.") XCTAssertNotNil(service.registrationWizard, "The registration wizard should exist after starting a registration flow.") XCTAssertNotNil(service.state.homeserver.registrationFlow, "The supported registration flow should be stored after starting a registration flow.") XCTAssertTrue(service.isRegistrationStarted, "The service should show as having started registration.") - XCTAssertEqual(service.state.flow, .registration, "The service should show as using a registration flow.") + XCTAssertEqual(service.state.flow, .register, "The service should show as using a registration flow.") // When resetting the service. service.reset() diff --git a/changelog.d/6046_uisi_context b/changelog.d/6046.change similarity index 100% rename from changelog.d/6046_uisi_context rename to changelog.d/6046.change diff --git a/changelog.d/6074.change b/changelog.d/6074.change new file mode 100644 index 000000000..fbcef7b9a --- /dev/null +++ b/changelog.d/6074.change @@ -0,0 +1 @@ +Authentication: New user accounts are now tracked in analytics if the user opted in. \ No newline at end of file diff --git a/changelog.d/6110.bugfix b/changelog.d/6110.bugfix new file mode 100644 index 000000000..a457ecedf --- /dev/null +++ b/changelog.d/6110.bugfix @@ -0,0 +1 @@ +Location sharing: fix stop button in timeline