Send the Signup analytics event (#6118)

* Implement the Signup event from AuthenticationViewController.

Use AuthenticationFlow instead of MXKAuthenticationType.
Create new AuthenticationType that aligns with AnalyticsEvent naming.
Add additional cases from AnalyticsEvents.
This commit is contained in:
Doug 2022-05-06 13:08:54 +01:00 committed by GitHub
parent f81bd5d656
commit 54a4feb0ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 232 additions and 54 deletions

View file

@ -183,7 +183,7 @@ EXTERNAL SOURCES:
CHECKOUT OPTIONS: CHECKOUT OPTIONS:
AnalyticsEvents: AnalyticsEvents:
:commit: f37a2f243270bffdcddfa5cbaeb4379e0db581c2 :commit: b275ccb194a219a61b3100159d51cadbf7c9020c
:git: https://github.com/matrix-org/matrix-analytics-events.git :git: https://github.com/matrix-org/matrix-analytics-events.git
SPEC CHECKSUMS: SPEC CHECKSUMS:

View file

@ -226,6 +226,13 @@ extension Analytics {
client.updateUserProperties(userProperties) 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 /// Track the presentation of a screen
/// - Parameters: /// - Parameters:
/// - screen: The screen that was shown. /// - screen: The screen that was shown.

View file

@ -26,6 +26,7 @@ import AnalyticsEvents
case slashCommand case slashCommand
case spaceHierarchy case spaceHierarchy
case timeline case timeline
case permalink
var trigger: AnalyticsEvent.JoinedRoom.Trigger? { var trigger: AnalyticsEvent.JoinedRoom.Trigger? {
switch self { switch self {
@ -45,6 +46,8 @@ import AnalyticsEvents
return .SpaceHierarchy return .SpaceHierarchy
case .timeline: case .timeline:
return .Timeline return .Timeline
case .permalink:
return .MobilePermalink
} }
} }
} }

View file

@ -39,6 +39,11 @@ import AnalyticsEvents
case inCall case inCall
case spaceMenu case spaceMenu
case spaceSettings case spaceSettings
case roomPreview
case permalink
case linkShare
case exploreRooms
case spaceMembers
var trigger: AnalyticsEvent.ViewRoom.Trigger? { var trigger: AnalyticsEvent.ViewRoom.Trigger? {
switch self { switch self {
@ -84,6 +89,16 @@ import AnalyticsEvents
return .MobileSpaceMenu return .MobileSpaceMenu
case .spaceSettings: case .spaceSettings:
return .MobileSpaceSettings return .MobileSpaceSettings
case .roomPreview:
return .MobileRoomPreview
case .permalink:
return .MobilePermalink
case .linkShare:
return .MobileLinkShare
case .exploreRooms:
return .MobileExploreRooms
case .spaceMembers:
return .MobileSpaceMembers
} }
} }
} }

View file

@ -21,7 +21,7 @@ import Foundation
enum AuthenticationCoordinatorResult { enum AuthenticationCoordinatorResult {
/// The user has authenticated but key verification is yet to happen. The session value is /// 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. /// 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. /// All of the required authentication steps including key verification is complete.
case didComplete case didComplete
} }
@ -34,7 +34,7 @@ protocol AuthenticationCoordinatorProtocol: Coordinator, Presentable {
var customServerFieldsVisible: Bool { get set } var customServerFieldsVisible: Bool { get set }
/// Update the screen to display registration or login. /// 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. /// Force a registration process based on a predefined set of parameters from a server provisioning link.
/// For more information see `AuthenticationViewController.externalRegistrationParameters`. /// For more information see `AuthenticationViewController.externalRegistrationParameters`.

View file

@ -19,6 +19,7 @@
#import "MatrixKit.h" #import "MatrixKit.h"
@protocol AuthenticationViewControllerDelegate; @protocol AuthenticationViewControllerDelegate;
@class SSOIdentityProvider;
@interface AuthenticationViewController : MXKAuthenticationViewController <MXKAuthenticationViewControllerDelegate> @interface AuthenticationViewController : MXKAuthenticationViewController <MXKAuthenticationViewControllerDelegate>
@ -60,8 +61,16 @@
@protocol AuthenticationViewControllerDelegate <NSObject> @protocol AuthenticationViewControllerDelegate <NSObject>
- (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 didLoginWithSession:(MXSession *)session
andPassword:(NSString *)password; andPassword:(NSString * _Nullable)password
orSSOIdentityProvider:(SSOIdentityProvider * _Nullable)identityProvider;
@end; @end;

View file

@ -76,6 +76,10 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
// Current SSO transaction id used to identify and validate the SSO authentication callback // Current SSO transaction id used to identify and validate the SSO authentication callback
@property (nonatomic, strong) NSString *ssoCallbackTxnId; @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; @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. // 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 #pragma mark - MXKAuthInputsViewDelegate
@ -1590,14 +1597,14 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
#pragma mark - SocialLoginListViewDelegate #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 #pragma mark - SSOIdentityProviderAuthenticationPresenter
- (void)presentSSOAuthenticationForIdentityProviderIdentifier:(NSString*)identityProviderIdentifier - (void)presentSSOAuthenticationForIdentityProvider:(SSOIdentityProvider*)identityProvider
{ {
NSString *homeServerStringURL = self.homeServerTextField.text; 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 // Generate a unique identifier that will identify the success callback URL
NSString *transactionId = [MXTools generateTransactionId]; 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.ssoCallbackTxnId = transactionId;
self.ssoAuthenticationPresenter = presenter; self.ssoAuthenticationPresenter = presenter;
@ -1623,7 +1630,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
- (void)presentDefaultSSOAuthentication - (void)presentDefaultSSOAuthentication
{ {
[self presentSSOAuthenticationForIdentityProviderIdentifier:nil]; [self presentSSOAuthenticationForIdentityProvider:nil];
} }
- (void)dismissSSOAuthenticationPresenter - (void)dismissSSOAuthenticationPresenter
@ -1656,8 +1663,11 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
[self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil]; [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 dismissSSOAuthenticationPresenter];
[self loginWithToken:token]; [self loginWithToken:token];
} }

View file

@ -81,8 +81,8 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
return self.authenticationViewController return self.authenticationViewController
} }
func update(authenticationType: MXKAuthenticationType) { func update(authenticationFlow: AuthenticationFlow) {
authenticationViewController.authType = authenticationType authenticationViewController.authType = authenticationFlow.mxkType
} }
func update(externalRegistrationParameters: [AnyHashable: Any]) { func update(externalRegistrationParameters: [AnyHashable: Any]) {
@ -149,7 +149,16 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
// MARK: - AuthenticationViewControllerDelegate // MARK: - AuthenticationViewControllerDelegate
extension LegacyAuthenticationCoordinator: 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 self.session = session
if canPresentAdditionalScreens { if canPresentAdditionalScreens {
@ -177,8 +186,18 @@ extension LegacyAuthenticationCoordinator: AuthenticationViewControllerDelegate
verificationListener.start() verificationListener.start()
self.verificationListener = verificationListener self.verificationListener = verificationListener
let authenticationType: AuthenticationType
if let identityProvider = identityProvider {
authenticationType = .sso(identityProvider)
} else if !password.isEmptyOrNil {
authenticationType = .password
} else {
authenticationType = .other
}
completion?(.didLogin(session: session, authenticationType: authenticationViewController.authType)) completion?(.didLogin(session: session,
authenticationFlow: authenticationViewController.authType.flow,
authenticationType: authenticationType))
} }
} }
@ -210,3 +229,29 @@ extension LegacyAuthenticationCoordinator: UIAdaptivePresentationControllerDeleg
return false 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
}
}
}

View file

@ -20,7 +20,9 @@ import SafariServices
@objc protocol SSOAuthenticationPresenterDelegate { @objc protocol SSOAuthenticationPresenterDelegate {
func ssoAuthenticationPresenterDidCancel(_ presenter: SSOAuthenticationPresenter) func ssoAuthenticationPresenterDidCancel(_ presenter: SSOAuthenticationPresenter)
func ssoAuthenticationPresenter(_ presenter: SSOAuthenticationPresenter, authenticationDidFailWithError error: Error) 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 { enum SSOAuthenticationPresenterError: Error {
@ -46,6 +48,7 @@ final class SSOAuthenticationPresenter: NSObject {
// MARK: Public // MARK: Public
private(set) var identityProvider: SSOIdentityProvider?
weak var delegate: SSOAuthenticationPresenterDelegate? weak var delegate: SSOAuthenticationPresenterDelegate?
// MARK: - Setup // MARK: - Setup
@ -57,15 +60,16 @@ final class SSOAuthenticationPresenter: NSObject {
// MARK: - Public // MARK: - Public
func present(forIdentityProviderIdentifier identityProviderIdentifier: String?, func present(forIdentityProvider identityProvider: SSOIdentityProvider?,
with transactionId: String, with transactionId: String,
from presentingViewController: UIViewController, from presentingViewController: UIViewController,
animated: Bool) { 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) self.delegate?.ssoAuthenticationPresenter(self, authenticationDidFailWithError: SSOAuthenticationPresenterError.failToLoadAuthenticationURL)
return return
} }
self.identityProvider = identityProvider
self.presentingViewController = presentingViewController 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. // 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 { } else if let successURL = callBackURL {
if let loginToken = self.ssoAuthenticationService.loginToken(from: successURL) { if let loginToken = self.ssoAuthenticationService.loginToken(from: successURL) {
self.delegate?.ssoAuthenticationPresenter(self, authenticationSucceededWithToken: loginToken) self.delegate?.ssoAuthenticationPresenter(self, authenticationSucceededWithToken: loginToken, usingIdentityProvider: self.identityProvider)
} else { } else {
MXLog.debug("SSOAuthenticationPresenter: Login token not found") MXLog.debug("SSOAuthenticationPresenter: Login token not found")
self.delegate?.ssoAuthenticationPresenter(self, authenticationDidFailWithError: SSOAuthenticationServiceError.tokenNotFound) self.delegate?.ssoAuthenticationPresenter(self, authenticationDidFailWithError: SSOAuthenticationServiceError.tokenNotFound)

View file

@ -42,7 +42,10 @@ final class SocialLoginButton: UIButton, Themable {
// MARK: Public // MARK: Public
var identifier: String? { var identifier: String? {
return self.viewData?.identifier viewData?.identityProvider.id
}
var identityProvider: SSOIdentityProvider? {
viewData?.identityProvider
} }
// MARK: Setup // MARK: Setup

View file

@ -71,7 +71,7 @@ class SocialLoginButtonFactory {
let title = self.buildButtonTitle(with: identityProvider.name, mode: mode) let title = self.buildButtonTitle(with: identityProvider.name, mode: mode)
let viewData = SocialLoginButtonViewData(identifier: identityProvider.identifier, let viewData = SocialLoginButtonViewData(identityProvider: identityProvider.ssoIdentityProvider,
title: title, title: title,
defaultStyle: defaultStyle, defaultStyle: defaultStyle,
themeStyles: styles) themeStyles: styles)

View file

@ -19,8 +19,8 @@ import Foundation
/// SocialLoginButton view data /// SocialLoginButton view data
struct SocialLoginButtonViewData { struct SocialLoginButtonViewData {
/// Identify provider identifier /// Identity provider identifier
let identifier: String let identityProvider: SSOIdentityProvider
/// Button title /// Button title
let title: String let title: String

View file

@ -18,7 +18,7 @@ import UIKit
import Reusable import Reusable
@objc protocol SocialLoginListViewDelegate: AnyObject { @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. /// 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 // MARK: - Action
@objc private func socialButtonAction(_ socialLoginButton: SocialLoginButton) { @objc private func socialButtonAction(_ socialLoginButton: SocialLoginButton) {
guard let identifier = socialLoginButton.identifier else { guard let provider = socialLoginButton.identityProvider else {
return return
} }
self.delegate?.socialLoginListView(self, didTapSocialButtonWithIdentifier: identifier) self.delegate?.socialLoginListView(self, didTapSocialButtonWithProvider: provider)
} }
} }

View file

@ -76,7 +76,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
func start() { func start() {
Task { Task {
do { 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) try await authenticationService.startFlow(flow, for: authenticationService.state.homeserver.address)
} catch { } catch {
MXLog.error("[AuthenticationCoordinator] start: Failed to start") MXLog.error("[AuthenticationCoordinator] start: Failed to start")
@ -211,7 +211,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
func handleRegistrationResult(_ result: RegistrationResult) { func handleRegistrationResult(_ result: RegistrationResult) {
switch result { switch result {
case .success(let mxSession): case .success(let mxSession):
onSessionCreated(session: mxSession, isAccountCreated: true) onSessionCreated(session: mxSession, flow: .register)
case .flowResponse(let flowResult): case .flowResponse(let flowResult):
// TODO // TODO
break break
@ -219,7 +219,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
} }
/// Handles the creation of a new session following on from a successful authentication. /// 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.session = session
// self.password = password // self.password = password
@ -249,7 +249,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
verificationListener.start() verificationListener.start()
self.verificationListener = verificationListener 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 // MARK: - Additional Screens
@ -331,7 +332,7 @@ extension AuthenticationCoordinator {
set { /* no-op */ } set { /* no-op */ }
} }
func update(authenticationType: MXKAuthenticationType) { func update(authenticationFlow: AuthenticationFlow) {
// unused // unused
} }

View file

@ -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
}
}
}

View file

@ -63,7 +63,10 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
// MARK: Screen results // MARK: Screen results
private var splashScreenResult: OnboardingSplashScreenViewModelResult? private var splashScreenResult: OnboardingSplashScreenViewModelResult?
private var useCaseResult: OnboardingUseCaseViewModelResult? 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? private var session: MXSession?
/// A place to store the image selected in the avatar screen until it has been saved. /// A place to store the image selected in the avatar screen until it has been saved.
private var selectedAvatar: UIImage? private var selectedAvatar: UIImage?
@ -154,7 +157,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
splashScreenResult = result splashScreenResult = result
// Set the auth type early to allow network requests to finish during display of the use case screen. // 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 { switch result {
case .register: case .register:
@ -219,8 +222,8 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
guard let self = self, let coordinator = coordinator else { return } guard let self = self, let coordinator = coordinator else { return }
switch result { switch result {
case .didLogin(let session, let authenticationType): case .didLogin(let session, let authenticationFlow, let authenticationType):
self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationType) self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationFlow, using: authenticationType)
case .didComplete: case .didComplete:
self.authenticationCoordinatorDidComplete(coordinator) self.authenticationCoordinatorDidComplete(coordinator)
} }
@ -241,8 +244,8 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
guard let self = self, let coordinator = coordinator else { return } guard let self = self, let coordinator = coordinator else { return }
switch result { switch result {
case .didLogin(let session, let authenticationType): case .didLogin(let session, let authenticationFlow, let authenticationType):
self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationType) self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationFlow, using: authenticationType)
case .didComplete: case .didComplete:
self.authenticationCoordinatorDidComplete(coordinator) 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. /// whilst crypto and the rest of the app is launching in the background.
private func authenticationCoordinator(_ coordinator: AuthenticationCoordinatorProtocol, private func authenticationCoordinator(_ coordinator: AuthenticationCoordinatorProtocol,
didLoginWith session: MXSession, didLoginWith session: MXSession,
and authenticationType: MXKAuthenticationType) { and authenticationFlow: AuthenticationFlow,
using authenticationType: AuthenticationType) {
self.session = session self.session = session
self.authenticationFlow = authenticationFlow
self.authenticationType = authenticationType self.authenticationType = authenticationType
// Check whether another screen should be shown. // Check whether another screen should be shown.
if #available(iOS 14.0, *) { if #available(iOS 14.0, *) {
if authenticationType == .register, if authenticationFlow == .register,
let userId = session.credentials.userId, let userId = session.credentials.userId,
let userSession = UserSessionsService.shared.userSession(withUserId: 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 // 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 isShowingLegacyAuthentication = false
// Handle the chosen use case where applicable // Handle the chosen use case where applicable
if authenticationType == .register, if authenticationFlow == .register,
let useCase = useCaseResult?.userSessionPropertyValue, let useCase = useCaseResult?.userSessionPropertyValue,
let userSession = UserSessionsService.shared.mainUserSession { let userSession = UserSessionsService.shared.mainUserSession {
// Store the value in the user's session // Store the value in the user's session
@ -559,15 +564,28 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
return return
} }
trackSignup()
completion?() 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 // MARK: - Helpers
extension OnboardingSplashScreenViewModelResult { extension OnboardingSplashScreenViewModelResult {
/// The result converted into the MatrixKit authentication type to use. /// The result converted into an authentication flow.
var mxkAuthenticationType: MXKAuthenticationType { var flow: AuthenticationFlow {
switch self { switch self {
case .login: case .login:
return .login return .login

View file

@ -16,10 +16,20 @@
import Foundation 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 { enum AuthenticationFlow {
case login 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`. /// 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. /// Represents an SSO Identity Provider as provided in a login flow.
struct SSOIdentityProvider: Identifiable { @objc class SSOIdentityProvider: NSObject, 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}`. /// The id field is the Identity Provider identifier used for the SSO Web page redirection `/login/sso/redirect/{idp_id}`.
let id: String let id: String
/// The name field is a human readable string intended to be printed by the client. /// The name field is a human readable string intended to be printed by the client.
let name: String let name: String
@ -74,4 +84,11 @@ struct SSOIdentityProvider: Identifiable {
let brand: String? 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. /// 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? let iconURL: String?
init(id: String, name: String, brand: String?, iconURL: String?) {
self.id = id
self.name = name
self.brand = brand
self.iconURL = iconURL
}
} }

View file

@ -98,7 +98,7 @@ class AuthenticationService: NSObject {
let loginWizard = LoginWizard() let loginWizard = LoginWizard()
self.loginWizard = loginWizard self.loginWizard = loginWizard
if flow == .registration { if flow == .register {
do { do {
let registrationWizard = RegistrationWizard(client: client) let registrationWizard = RegistrationWizard(client: client)
state.homeserver.registrationFlow = try await registrationWizard.registrationFlow() state.homeserver.registrationFlow = try await registrationWizard.registrationFlow()
@ -124,7 +124,7 @@ class AuthenticationService: NSObject {
switch flow { switch flow {
case .login: case .login:
return client.loginFallbackURL return client.loginFallbackURL
case .registration: case .register:
return client.registerFallbackURL return client.registerFallbackURL
} }
} }

View file

@ -119,7 +119,7 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable {
Task { Task {
do { do {
#warning("The screen should be configuration for .login too.") #warning("The screen should be configuration for .login too.")
try await authenticationService.startFlow(.registration, for: homeserverAddress) try await authenticationService.startFlow(.register, for: homeserverAddress)
stopLoading() stopLoading()
completion?(.updated) completion?(.updated)

View file

@ -39,7 +39,7 @@ class AuthenticationServiceTests: XCTestCase {
XCTAssertNil(service.state.homeserver.registrationFlow, "A new service shouldn't provide a registration flow for the homeserver.") XCTAssertNil(service.state.homeserver.registrationFlow, "A new service shouldn't provide a registration flow for the homeserver.")
// When starting a new registration flow. // 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. // Then a registration wizard should be available for use.
XCTAssertNotNil(service.registrationWizard, "The registration wizard should exist after starting a registration flow.") XCTAssertNotNil(service.registrationWizard, "The registration wizard should exist after starting a registration flow.")
@ -49,13 +49,13 @@ class AuthenticationServiceTests: XCTestCase {
func testReset() async throws { func testReset() async throws {
// Given a service that has begun registration. // Given a service that has begun registration.
let service = AuthenticationService() 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") _ = 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.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.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.") 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.") 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. // When resetting the service.
service.reset() service.reset()

1
changelog.d/6074.change Normal file
View file

@ -0,0 +1 @@
Authentication: New user accounts are now tracked in analytics if the user opted in.