diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift
index 3348ba964..59db9a270 100644
--- a/Config/BuildSettings.swift
+++ b/Config/BuildSettings.swift
@@ -361,6 +361,9 @@ final class BuildSettings: NSObject {
// MARK: - Authentication Options
static let authEnableRefreshTokens = false
+ // MARK: - Onboarding
+ static let onboardingShowAccountPersonalisation = false
+
// MARK: - Unified Search
static let unifiedSearchScreenShowPublicDirectory = true
diff --git a/Riot/Assets/Images.xcassets/Onboarding/onboarding_congratulations_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Onboarding/onboarding_congratulations_icon.imageset/Contents.json
new file mode 100644
index 000000000..abd9b2f32
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Onboarding/onboarding_congratulations_icon.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "user.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Onboarding/onboarding_congratulations_icon.imageset/user.svg b/Riot/Assets/Images.xcassets/Onboarding/onboarding_congratulations_icon.imageset/user.svg
new file mode 100644
index 000000000..0321e191f
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Onboarding/onboarding_congratulations_icon.imageset/user.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings
index 63a0afa4d..9d3821746 100644
--- a/Riot/Assets/en.lproj/Vector.strings
+++ b/Riot/Assets/en.lproj/Vector.strings
@@ -104,6 +104,11 @@
"onboarding_use_case_existing_server_message" = "Looking to join an existing server?";
"onboarding_use_case_existing_server_button" = "Connect to server";
+"onboarding_congratulations_title" = "Congratulations!";
+"onboarding_congratulations_message" = "Your account\n%@\nhas been created.";
+"onboarding_congratulations_personalise_button" = "Personalise profile";
+"onboarding_congratulations_home_button" = "Take me home";
+
// Authentication
"auth_login" = "Log in";
"auth_register" = "Register";
diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift
index ee18b509e..65511e31c 100644
--- a/Riot/Generated/Images.swift
+++ b/Riot/Generated/Images.swift
@@ -123,6 +123,7 @@ internal class Asset: NSObject {
internal static let onboardingSplashScreenPage3Dark = ImageAsset(name: "OnboardingSplashScreenPage3Dark")
internal static let onboardingSplashScreenPage4 = ImageAsset(name: "OnboardingSplashScreenPage4")
internal static let onboardingSplashScreenPage4Dark = ImageAsset(name: "OnboardingSplashScreenPage4Dark")
+ internal static let onboardingCongratulationsIcon = ImageAsset(name: "onboarding_congratulations_icon")
internal static let onboardingUseCaseCommunity = ImageAsset(name: "onboarding_use_case_community")
internal static let onboardingUseCaseCommunityDark = ImageAsset(name: "onboarding_use_case_community_dark")
internal static let onboardingUseCaseIcon = ImageAsset(name: "onboarding_use_case_icon")
diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift
index 84b6970a6..98b157ed8 100644
--- a/Riot/Generated/Strings.swift
+++ b/Riot/Generated/Strings.swift
@@ -2407,6 +2407,22 @@ public class VectorL10n: NSObject {
public static var on: String {
return VectorL10n.tr("Vector", "on")
}
+ /// Take me home
+ public static var onboardingCongratulationsHomeButton: String {
+ return VectorL10n.tr("Vector", "onboarding_congratulations_home_button")
+ }
+ /// Your account\n%@\nhas been created.
+ public static func onboardingCongratulationsMessage(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "onboarding_congratulations_message", p1)
+ }
+ /// Personalise profile
+ public static var onboardingCongratulationsPersonaliseButton: String {
+ return VectorL10n.tr("Vector", "onboarding_congratulations_personalise_button")
+ }
+ /// Congratulations!
+ public static var onboardingCongratulationsTitle: String {
+ return VectorL10n.tr("Vector", "onboarding_congratulations_title")
+ }
/// I already have an account
public static var onboardingSplashLoginButtonTitle: String {
return VectorL10n.tr("Vector", "onboarding_splash_login_button_title")
diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift
index 865161941..a98f95cc1 100644
--- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift
+++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift
@@ -28,6 +28,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
private let navigationRouter: NavigationRouterType
private let authenticationViewController: AuthenticationViewController
+ private var canPresentAdditionalScreens: Bool
+ private var isWaitingToPresentCompleteSecurity = false
private let crossSigningService = CrossSigningService()
/// The password entered, for use when setting up cross-signing.
@@ -45,6 +47,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
init(parameters: AuthenticationCoordinatorParameters) {
self.navigationRouter = parameters.navigationRouter
+ self.canPresentAdditionalScreens = parameters.canPresentAdditionalScreens
let authenticationViewController = AuthenticationViewController()
self.authenticationViewController = authenticationViewController
@@ -91,6 +94,17 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
authenticationViewController.continueSSOLogin(withToken: loginToken, txnId: transactionID)
}
+ func allowScreenPresentation() {
+ canPresentAdditionalScreens = true
+
+ showLoadingAnimation()
+
+ if isWaitingToPresentCompleteSecurity {
+ isWaitingToPresentCompleteSecurity = false
+ presentCompleteSecurity()
+ }
+ }
+
// MARK: - Private
private func showLoadingAnimation() {
@@ -102,7 +116,13 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
navigationRouter.setRootModule(loadingViewController)
}
- private func presentCompleteSecurity(with session: MXSession) {
+ private func presentCompleteSecurity() {
+ guard let session = session else {
+ MXLog.error("[AuthenticationCoordinator] presentCompleteSecurity: Unable to present security due to missing session.")
+ authenticationDidComplete()
+ return
+ }
+
let isNewSignIn = true
let keyVerificationCoordinator = KeyVerificationCoordinator(session: session, flow: .completeSecurity(isNewSignIn))
@@ -115,7 +135,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
}
private func authenticationDidComplete() {
- completion?(.didComplete(authenticationViewController.authType))
+ completion?(.didComplete)
}
private func registerSessionStateChangeNotification(for session: MXSession) {
@@ -183,8 +203,14 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
self.authenticationDidComplete()
}
case .crossSigningExists:
+ guard self.canPresentAdditionalScreens else {
+ MXLog.debug("[AuthenticationCoordinator] sessionStateDidChange: Delaying presentCompleteSecurity during onboarding.")
+ self.isWaitingToPresentCompleteSecurity = true
+ return
+ }
+
MXLog.debug("[AuthenticationCoordinator] sessionStateDidChange: Complete security")
- self.presentCompleteSecurity(with: session)
+ self.presentCompleteSecurity()
default:
MXLog.debug("[AuthenticationCoordinator] sessionStateDidChange: Nothing to do")
@@ -211,8 +237,10 @@ extension AuthenticationCoordinator: AuthenticationViewControllerDelegate {
self.session = session
self.password = password
- self.showLoadingAnimation()
- completion?(.didLogin(session))
+ if canPresentAdditionalScreens {
+ showLoadingAnimation()
+ }
+ completion?(.didLogin(session: session, authenticationType: authenticationViewController.authType))
}
}
diff --git a/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift b/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift
index c024cddbe..95d97146d 100644
--- a/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift
+++ b/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift
@@ -20,14 +20,16 @@ import Foundation
struct AuthenticationCoordinatorParameters {
let navigationRouter: NavigationRouterType
+ /// Whether or not the coordinator should show the loading spinner, key verification etc.
+ let canPresentAdditionalScreens: Bool
}
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(MXSession)
+ case didLogin(session: MXSession, authenticationType: MXKAuthenticationType)
/// All of the required authentication steps including key verification is complete.
- case didComplete(MXKAuthenticationType)
+ case didComplete
}
/// `AuthenticationCoordinatorProtocol` is a protocol describing a Coordinator that handle's the authentication navigation flow.
@@ -52,4 +54,8 @@ protocol AuthenticationCoordinatorProtocol: Coordinator, Presentable {
/// When SSO login succeeded, when SFSafariViewController is used, continue login with success parameters.
func continueSSOLogin(withToken loginToken: String, transactionID: String) -> Bool
+
+ /// Indicates to the coordinator to display any pending screens if it was created with
+ /// the `canPresentAdditionalScreens` parameter set to `false`
+ func allowScreenPresentation()
}
diff --git a/Riot/Modules/MatrixKit/Views/Authentication/MXKAuthInputsView.h b/Riot/Modules/MatrixKit/Views/Authentication/MXKAuthInputsView.h
index c71b022ae..5582aa95a 100644
--- a/Riot/Modules/MatrixKit/Views/Authentication/MXKAuthInputsView.h
+++ b/Riot/Modules/MatrixKit/Views/Authentication/MXKAuthInputsView.h
@@ -25,7 +25,7 @@ extern NSString *const MXKAuthErrorDomain;
/**
Authentication types
*/
-typedef enum {
+typedef NS_ENUM(NSUInteger, MXKAuthenticationType) {
/**
Type used to sign up.
*/
@@ -39,7 +39,7 @@ typedef enum {
*/
MXKAuthenticationTypeForgotPassword
-} MXKAuthenticationType;
+};
@class MXKAuthInputsView;
diff --git a/Riot/Modules/Onboarding/OnboardingCoordinator.swift b/Riot/Modules/Onboarding/OnboardingCoordinator.swift
index dd7f088e7..fcd3c6ead 100644
--- a/Riot/Modules/Onboarding/OnboardingCoordinator.swift
+++ b/Riot/Modules/Onboarding/OnboardingCoordinator.swift
@@ -61,6 +61,13 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
// MARK: Screen results
private var splashScreenResult: OnboardingSplashScreenViewModelResult?
private var useCaseResult: OnboardingUseCaseViewModelResult?
+ private var authenticationType: MXKAuthenticationType?
+ private var session: MXSession?
+
+ /// Whether all of the onboarding steps have been completed or not. `false` if there are more screens to be shown.
+ private var onboardingIsFinished = false
+ /// Whether the main app is ready to be shown or not. `true` once authenticated, verified and the store/data sources are ready.
+ private var appIsReady = false
// MARK: Public
@@ -74,7 +81,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
self.parameters = parameters
// Preload the authVC (it is *really* slow to load in realtime)
- let authenticationParameters = AuthenticationCoordinatorParameters(navigationRouter: parameters.router)
+ let authenticationParameters = AuthenticationCoordinatorParameters(navigationRouter: parameters.router, canPresentAdditionalScreens: false)
authenticationCoordinator = AuthenticationCoordinator(parameters: authenticationParameters)
super.init()
@@ -115,11 +122,13 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
return authenticationCoordinator.continueSSOLogin(withToken: loginToken, transactionID: transactionID)
}
- // MARK: - Private
+ // MARK: - Pre-Authentication
@available(iOS 14.0, *)
/// Show the onboarding splash screen as the root module in the flow.
private func showSplashScreen() {
+ MXLog.debug("[OnboardingCoordinator] showSplashScreen")
+
let coordinator = OnboardingSplashScreenCoordinator()
coordinator.completion = { [weak self, weak coordinator] result in
guard let self = self, let coordinator = coordinator else { return }
@@ -129,7 +138,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
coordinator.start()
add(childCoordinator: coordinator)
- self.navigationRouter.setRootModule(coordinator, popCompletion: nil)
+ navigationRouter.setRootModule(coordinator, popCompletion: nil)
}
@available(iOS 14.0, *)
@@ -138,8 +147,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.
- let mxkAuthenticationType = splashScreenResult == .register ? MXKAuthenticationTypeRegister : MXKAuthenticationTypeLogin
- authenticationCoordinator.update(authenticationType: mxkAuthenticationType)
+ authenticationCoordinator.update(authenticationType: result.mxkAuthenticationType)
switch result {
case .register:
@@ -152,6 +160,8 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
@available(iOS 14.0, *)
/// Show the use case screen for new users.
private func showUseCaseSelectionScreen() {
+ MXLog.debug("[OnboardingCoordinator] showUseCaseSelectionScreen")
+
let coordinator = OnboardingUseCaseSelectionCoordinator()
coordinator.completion = { [weak self, weak coordinator] result in
guard let self = self, let coordinator = coordinator else { return }
@@ -161,10 +171,10 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
coordinator.start()
add(childCoordinator: coordinator)
- if self.navigationRouter.modules.isEmpty {
- self.navigationRouter.setRootModule(coordinator, popCompletion: nil)
+ if navigationRouter.modules.isEmpty {
+ navigationRouter.setRootModule(coordinator, popCompletion: nil)
} else {
- self.navigationRouter.push(coordinator, animated: true) { [weak self] in
+ navigationRouter.push(coordinator, animated: true) { [weak self] in
self?.remove(childCoordinator: coordinator)
}
}
@@ -176,6 +186,8 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
showAuthenticationScreen()
}
+ // MARK: - Authentication
+
/// Show the authentication screen. Any parameters that have been set in previous screens are be applied.
private func showAuthenticationScreen() {
guard !isShowingAuthentication else { return }
@@ -187,10 +199,10 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
guard let self = self, let coordinator = coordinator else { return }
switch result {
- case .didLogin(let session):
- self.authenticationCoordinator(coordinator, didLoginWith: session)
- case .didComplete(let authenticationType):
- self.authenticationCoordinator(coordinator, didCompleteWith: authenticationType)
+ case .didLogin(let session, let authenticationType):
+ self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationType)
+ case .didComplete:
+ self.authenticationCoordinatorDidComplete(coordinator)
}
}
@@ -217,10 +229,10 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
coordinator.updateHomeserver(customHomeserver, andIdentityServer: customIdentityServer)
}
- if self.navigationRouter.modules.isEmpty {
- self.navigationRouter.setRootModule(coordinator, popCompletion: nil)
+ if navigationRouter.modules.isEmpty {
+ navigationRouter.setRootModule(coordinator, popCompletion: nil)
} else {
- self.navigationRouter.push(coordinator, animated: true) { [weak self] in
+ navigationRouter.push(coordinator, animated: true) { [weak self] in
self?.remove(childCoordinator: coordinator)
self?.isShowingAuthentication = false
}
@@ -228,18 +240,38 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
isShowingAuthentication = true
}
- private func authenticationCoordinator(_ coordinator: AuthenticationCoordinatorProtocol, didLoginWith session: MXSession) {
- // TODO: Show next screens whilst waiting for the everything to load.
+ /// Displays the next view in the flow after the authentication screen,
+ /// whilst crypto and the rest of the app is launching in the background.
+ private func authenticationCoordinator(_ coordinator: AuthenticationCoordinatorProtocol,
+ didLoginWith session: MXSession,
+ and authenticationType: MXKAuthenticationType) {
+ self.session = session
+ self.authenticationType = authenticationType
+
// May need to move the spinner and key verification up to here in order to coordinate properly.
+
+ // Check whether another screen should be shown.
+ if #available(iOS 14.0, *) {
+ if authenticationType == .register, let userId = session.credentials.userId, BuildSettings.onboardingShowAccountPersonalisation {
+ showCongratulationsScreen(userId: userId)
+ return
+ } else if Analytics.shared.shouldShowAnalyticsPrompt {
+ showAnalyticsPrompt(for: session)
+ return
+ }
+ }
+
+ // Otherwise onboarding is finished.
+ onboardingIsFinished = true
+ completeIfReady()
}
/// Displays the next view in the flow after the authentication screen.
- private func authenticationCoordinator(_ coordinator: AuthenticationCoordinatorProtocol, didCompleteWith authenticationType: MXKAuthenticationType) {
- completion?()
+ private func authenticationCoordinatorDidComplete(_ coordinator: AuthenticationCoordinatorProtocol) {
isShowingAuthentication = false
// Handle the chosen use case where applicable
- if authenticationType == MXKAuthenticationTypeRegister,
+ if authenticationType == .register,
let useCase = useCaseResult?.userSessionPropertyValue,
let userSession = UserSessionsService.shared.mainUserSession {
// Store the value in the user's session
@@ -248,6 +280,112 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
// Update the analytics user properties with the use case
Analytics.shared.updateUserProperties(ftueUseCase: useCase)
}
+
+ // This method is only called when the app is ready so we can complete if finished
+ appIsReady = true
+ completeIfReady()
+ }
+
+ // MARK: - Post-Authentication
+
+ @available(iOS 14.0, *)
+ private func showCongratulationsScreen(userId: String) {
+ MXLog.debug("[OnboardingCoordinator] showCongratulationsScreen")
+
+ let parameters = OnboardingCongratulationsCoordinatorParameters(userId: userId)
+ let coordinator = OnboardingCongratulationsCoordinator(parameters: parameters)
+
+ coordinator.completion = { [weak self, weak coordinator] result in
+ guard let self = self, let coordinator = coordinator else { return }
+ self.congratulationsCoordinator(coordinator, didCompleteWith: result)
+ }
+
+ add(childCoordinator: coordinator)
+ coordinator.start()
+
+ // Navigating back doesn't make any sense now, so replace the whole stack.
+ navigationRouter.setRootModule(coordinator, hideNavigationBar: true, animated: true) { [weak self] in
+ self?.remove(childCoordinator: coordinator)
+ }
+ }
+
+ @available(iOS 14.0, *)
+ private func congratulationsCoordinator(_ coordinator: OnboardingCongratulationsCoordinator, didCompleteWith result: OnboardingCongratulationsViewModelResult) {
+ if let session = session {
+ switch result {
+ case .personaliseProfile:
+ // TODO: Profile screens here instead.
+ if Analytics.shared.shouldShowAnalyticsPrompt {
+ showAnalyticsPrompt(for: session)
+ return
+ }
+ case .takeMeHome:
+ if Analytics.shared.shouldShowAnalyticsPrompt {
+ showAnalyticsPrompt(for: session)
+ return
+ }
+ }
+ }
+
+ onboardingIsFinished = true
+ completeIfReady()
+ }
+
+ @available(iOS 14.0, *)
+ private func showAnalyticsPrompt(for session: MXSession) {
+ MXLog.debug("[OnboardingCoordinator]: Invite the user to send analytics")
+
+ let parameters = AnalyticsPromptCoordinatorParameters(session: session)
+ let coordinator = AnalyticsPromptCoordinator(parameters: parameters)
+
+ coordinator.completion = { [weak self, weak coordinator] in
+ guard let self = self, let coordinator = coordinator else { return }
+ self.analyticsPromptCoordinatorDidComplete(coordinator)
+ }
+
+ add(childCoordinator: coordinator)
+ coordinator.start()
+
+ // TODO: Re-asses replacing the stack based on the previous screen once the whole flow is implemented
+ navigationRouter.setRootModule(coordinator, hideNavigationBar: true, animated: true) { [weak self] in
+ self?.remove(childCoordinator: coordinator)
+ }
+ }
+
+ private func analyticsPromptCoordinatorDidComplete(_ coordinator: AnalyticsPromptCoordinator) {
+ onboardingIsFinished = true
+ completeIfReady()
+ }
+
+ // MARK: - Finished
+
+ private func completeIfReady() {
+ guard onboardingIsFinished else {
+ MXLog.debug("[OnboardingCoordinator] Delaying onboarding completion until all screens have been shown.")
+ return
+ }
+
+ guard appIsReady else {
+ MXLog.debug("[OnboardingCoordinator] Allowing AuthenticationCoordinator to display any remaining screens.")
+ authenticationCoordinator.allowScreenPresentation()
+ return
+ }
+
+ completion?()
+ }
+}
+
+// MARK: - Helpers
+
+extension OnboardingSplashScreenViewModelResult {
+ /// The result converted into the MatrixKit authentication type to use.
+ var mxkAuthenticationType: MXKAuthenticationType {
+ switch self {
+ case .login:
+ return .login
+ case .register:
+ return .register
+ }
}
}
diff --git a/Riot/Modules/TabBar/MasterTabBarController.h b/Riot/Modules/TabBar/MasterTabBarController.h
index 6734a1528..c9948baf9 100644
--- a/Riot/Modules/TabBar/MasterTabBarController.h
+++ b/Riot/Modules/TabBar/MasterTabBarController.h
@@ -194,6 +194,5 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) {
- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectRoomPreviewWithParameters:(RoomPreviewNavigationParameters*)roomPreviewNavigationParameters completion:(void (^)(void))completion;
- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectContact:(MXKContact*)contact withPresentationParameters:(ScreenPresentationParameters*)presentationParameters;
- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectGroup:(MXGroup*)group inMatrixSession:(MXSession*)matrixSession presentationParameters:(ScreenPresentationParameters*)presentationParameters;
-- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController shouldPresentAnalyticsPromptForMatrixSession:(MXSession*)matrixSession;
@end
diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m
index 1a08218b9..bc306b757 100644
--- a/Riot/Modules/TabBar/MasterTabBarController.m
+++ b/Riot/Modules/TabBar/MasterTabBarController.m
@@ -73,11 +73,6 @@
@property (nonatomic) BOOL reviewSessionAlertHasBeenDisplayed;
-/**
- A flag to indicate that the analytics prompt should be shown during `-addMatrixSession:`.
- */
-@property(nonatomic) BOOL presentAnalyticsPromptOnAddSession;
-
@end
@implementation MasterTabBarController
@@ -204,20 +199,6 @@
if (!authIsShown)
{
- // Check whether the user should be prompted to send analytics.
- if (Analytics.shared.shouldShowAnalyticsPrompt)
- {
- MXSession *mxSession = self.mxSessions.firstObject;
- if (mxSession)
- {
- [self promptUserBeforeUsingAnalyticsForSession:mxSession];
- }
- else
- {
- self.presentAnalyticsPromptOnAddSession = YES;
- }
- }
-
[self refreshTabBarBadges];
// Release properly pushed and/or presented view controller
@@ -422,12 +403,6 @@
return;
}
- if (self.presentAnalyticsPromptOnAddSession)
- {
- self.presentAnalyticsPromptOnAddSession = NO;
- [self promptUserBeforeUsingAnalyticsForSession:mxSession];
- }
-
// Check whether the controller's view is loaded into memory.
if (self.homeViewController)
{
@@ -965,18 +940,6 @@
return NSNotFound;
}
-#pragma mark -
-
-- (void)promptUserBeforeUsingAnalyticsForSession:(MXSession *)mxSession
-{
- // Analytics aren't collected on iOS 12 & 13.
- if (@available(iOS 14.0, *))
- {
- MXLogDebug(@"[MasterTabBarController]: Invite the user to send analytics");
- [self.masterTabBarDelegate masterTabBarController:self shouldPresentAnalyticsPromptForMatrixSession:mxSession];
- }
-}
-
#pragma mark - Review session
- (void)presentVerifyCurrentSessionAlertIfNeededWithSession:(MXSession*)session
diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift
index 30ce90bc0..2cd8d408f 100644
--- a/Riot/Modules/TabBar/TabBarCoordinator.swift
+++ b/Riot/Modules/TabBar/TabBarCoordinator.swift
@@ -575,24 +575,6 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
self.splitViewMasterPresentableDelegate?.splitViewMasterPresentableWantsToResetDetail(self)
}
- @available(iOS 14.0, *)
- private func presentAnalyticsPrompt(with session: MXSession) {
- let parameters = AnalyticsPromptCoordinatorParameters(session: session)
- let coordinator = AnalyticsPromptCoordinator(parameters: parameters)
-
- coordinator.completion = { [weak self, weak coordinator] in
- guard let self = self, let coordinator = coordinator else { return }
-
- self.navigationRouter.dismissModule(animated: true, completion: nil)
- self.remove(childCoordinator: coordinator)
- }
-
- add(childCoordinator: coordinator)
-
- navigationRouter.present(coordinator, animated: true)
- coordinator.start()
- }
-
// MARK: UserSessions management
private func registerUserSessionsServiceNotifications() {
@@ -684,12 +666,6 @@ extension TabBarCoordinator: MasterTabBarControllerDelegate {
self.masterTabBarController.navigationItem.leftBarButtonItem = sideMenuBarButtonItem
}
-
- func masterTabBarController(_ masterTabBarController: MasterTabBarController!, shouldPresentAnalyticsPromptForMatrixSession matrixSession: MXSession!) {
- if #available(iOS 14.0, *) {
- presentAnalyticsPrompt(with: matrixSession)
- }
- }
}
// MARK: - RoomCoordinatorDelegate
diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift
index 178d1d215..816498006 100644
--- a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift
+++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift
@@ -125,6 +125,8 @@ struct AnalyticsPrompt: View {
.background(theme.colors.background.ignoresSafeArea())
.accentColor(theme.colors.accent)
}
+ .navigationBarHidden(true)
+ .navigationBarBackButtonHidden(true)
}
}
diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
index 242e4c088..73981575f 100644
--- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
+++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
@@ -20,6 +20,7 @@ import Foundation
@available(iOS 14.0, *)
enum MockAppScreens {
static let appScreens: [MockScreenState.Type] = [
+ MockOnboardingCongratulationsScreenState.self,
MockOnboardingUseCaseSelectionScreenState.self,
MockOnboardingSplashScreenScreenState.self,
MockLocationSharingScreenState.self,
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift
new file mode 100644
index 000000000..2ad988a7a
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift
@@ -0,0 +1,64 @@
+//
+// Copyright 2021 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 SwiftUI
+
+struct OnboardingCongratulationsCoordinatorParameters {
+ let userId: String
+}
+
+final class OnboardingCongratulationsCoordinator: Coordinator, Presentable {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let parameters: OnboardingCongratulationsCoordinatorParameters
+ private let onboardingCongratulationsHostingController: UIViewController
+ private var onboardingCongratulationsViewModel: OnboardingCongratulationsViewModelProtocol
+
+ // MARK: Public
+
+ // Must be used only internally
+ var childCoordinators: [Coordinator] = []
+ var completion: ((OnboardingCongratulationsViewModelResult) -> Void)?
+
+ // MARK: - Setup
+
+ @available(iOS 14.0, *)
+ init(parameters: OnboardingCongratulationsCoordinatorParameters) {
+ self.parameters = parameters
+
+ let viewModel = OnboardingCongratulationsViewModel(userId: parameters.userId)
+ let view = OnboardingCongratulations(viewModel: viewModel.context)
+ onboardingCongratulationsViewModel = viewModel
+ onboardingCongratulationsHostingController = VectorHostingController(rootView: view)
+ }
+
+ // MARK: - Public
+ func start() {
+ MXLog.debug("[OnboardingCongratulationsCoordinator] did start.")
+ onboardingCongratulationsViewModel.completion = { [weak self] result in
+ MXLog.debug("[OnboardingCongratulationsCoordinator] OnboardingCongratulationsViewModel did complete with result: \(result).")
+ guard let self = self else { return }
+ self.completion?(result)
+ }
+ }
+
+ func toPresentable() -> UIViewController {
+ return self.onboardingCongratulationsHostingController
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/MockOnboardingCongratulationsScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/MockOnboardingCongratulationsScreenState.swift
new file mode 100644
index 000000000..4aa617f1c
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/MockOnboardingCongratulationsScreenState.swift
@@ -0,0 +1,46 @@
+//
+// Copyright 2021 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 Foundation
+import SwiftUI
+
+/// Using an enum for the screen allows you define the different state cases with
+/// the relevant associated data for each case.
+@available(iOS 14.0, *)
+enum MockOnboardingCongratulationsScreenState: MockScreenState, CaseIterable {
+ // A case for each state you want to represent
+ // with specific, minimal associated data that will allow you
+ // mock that screen.
+ case congratulations
+
+ /// The associated screen
+ var screenType: Any.Type {
+ OnboardingCongratulationsScreen.self
+ }
+
+ /// Generate the view struct for the screen state.
+ var screenView: ([Any], AnyView) {
+ let viewModel = OnboardingCongratulationsViewModel(userId: "@testuser:example.com")
+
+ // can simulate service and viewModel actions here if needs be.
+
+ return (
+ [self, viewModel],
+ AnyView(OnboardingCongratulationsScreen(viewModel: viewModel.context)
+ .addDependency(MockAvatarService.example))
+ )
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift
new file mode 100644
index 000000000..d8eb9e926
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift
@@ -0,0 +1,37 @@
+//
+// Copyright 2021 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 Foundation
+
+// MARK: - Coordinator
+
+// MARK: View model
+
+enum OnboardingCongratulationsViewModelResult {
+ case personaliseProfile
+ case takeMeHome
+}
+
+// MARK: View
+
+struct OnboardingCongratulationsViewState: BindableState {
+ var userId: String
+}
+
+enum OnboardingCongratulationsViewAction {
+ case personaliseProfile
+ case takeMeHome
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModel.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModel.swift
new file mode 100644
index 000000000..26a7ebfdd
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModel.swift
@@ -0,0 +1,50 @@
+//
+// Copyright 2021 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 SwiftUI
+
+@available(iOS 14, *)
+typealias OnboardingCongratulationsViewModelType = StateStoreViewModel
+@available(iOS 14, *)
+class OnboardingCongratulationsViewModel: OnboardingCongratulationsViewModelType, OnboardingCongratulationsViewModelProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ // MARK: Public
+
+ var completion: ((OnboardingCongratulationsViewModelResult) -> Void)?
+
+ // MARK: - Setup
+
+ init(userId: String, initialCount: Int = 0) {
+ super.init(initialViewState: OnboardingCongratulationsViewState(userId: userId))
+ }
+
+ // MARK: - Public
+
+ override func process(viewAction: OnboardingCongratulationsViewAction) {
+ switch viewAction {
+ case .personaliseProfile:
+ completion?(.personaliseProfile)
+ case .takeMeHome:
+ completion?(.takeMeHome)
+ }
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModelProtocol.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModelProtocol.swift
new file mode 100644
index 000000000..72bde393e
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModelProtocol.swift
@@ -0,0 +1,24 @@
+//
+// Copyright 2021 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 Foundation
+
+protocol OnboardingCongratulationsViewModelProtocol {
+
+ var completion: ((OnboardingCongratulationsViewModelResult) -> Void)? { get set }
+ @available(iOS 14, *)
+ var context: OnboardingCongratulationsViewModelType.Context { get }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift
new file mode 100644
index 000000000..5b46e5c97
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift
@@ -0,0 +1,40 @@
+//
+// Copyright 2021 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 XCTest
+import RiotSwiftUI
+
+@available(iOS 14.0, *)
+class OnboardingCongratulationsUITests: MockScreenTest {
+
+ override class var screenType: MockScreenState.Type {
+ return MockOnboardingCongratulationsScreenState.self
+ }
+
+ override class func createTest() -> MockScreenTest {
+ return OnboardingCongratulationsUITests(selector: #selector(verifyOnboardingCongratulationsScreen))
+ }
+
+ func verifyOnboardingCongratulationsScreen() throws {
+ guard let screenState = screenState as? MockOnboardingCongratulationsScreenState else { fatalError("no screen") }
+ switch screenState {
+ case .congratulations:
+ // There isn't anything to test here
+ break
+ }
+ }
+
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/Unit/OnboardingCongratulationsViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/Unit/OnboardingCongratulationsViewModelTests.swift
new file mode 100644
index 000000000..44352f799
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/Unit/OnboardingCongratulationsViewModelTests.swift
@@ -0,0 +1,24 @@
+//
+// Copyright 2021 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 XCTest
+
+@testable import RiotSwiftUI
+
+@available(iOS 14.0, *)
+class OnboardingCongratulationsViewModelTests: XCTestCase {
+ // The view modal has minimal set up and no mutation so nothing to test.
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift
new file mode 100644
index 000000000..e7b6e5b13
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift
@@ -0,0 +1,108 @@
+//
+// Copyright 2021 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 SwiftUI
+
+@available(iOS 14.0, *)
+struct OnboardingCongratulationsScreen: View {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ @Environment(\.theme) private var theme
+ @Environment(\.horizontalSizeClass) private var horizontalSizeClass
+
+ private var horizontalPadding: CGFloat {
+ horizontalSizeClass == .regular ? 50 : 16
+ }
+
+ // MARK: Public
+
+ @ObservedObject var viewModel: OnboardingCongratulationsViewModel.Context
+
+ // MARK: Views
+
+ var body: some View {
+ GeometryReader { geometry in
+ VStack {
+ mainContent
+ .padding(.top, 60)
+ .padding(.horizontal, horizontalPadding)
+
+ Spacer()
+
+ buttons
+ .padding(.horizontal, horizontalPadding)
+ .padding(.bottom, 24)
+ .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
+ }
+ .frame(maxWidth: OnboardingConstants.maxContentWidth,
+ maxHeight: OnboardingConstants.maxContentHeight)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ .background(theme.colors.accent.ignoresSafeArea())
+ .accentColor(.white)
+ .navigationBarHidden(true)
+ .preferredColorScheme(.dark) // make the status bar white
+ }
+
+ /// The main content of the view to be shown in a scroll view.
+ var mainContent: some View {
+ VStack(spacing: 62) {
+ Image(Asset.Images.onboardingCongratulationsIcon.name)
+
+ VStack(spacing: 8) {
+ Text(VectorL10n.onboardingCongratulationsTitle)
+ .font(theme.fonts.title2B)
+ .foregroundColor(.white)
+
+ Text(VectorL10n.onboardingCongratulationsMessage(viewModel.viewState.userId))
+ .font(theme.fonts.body)
+ .foregroundColor(.white)
+ .multilineTextAlignment(.center)
+ }
+ }
+ }
+
+ /// The action buttons shown at the bottom of the view.
+ var buttons: some View {
+ VStack(spacing: 12) {
+ Button { viewModel.send(viewAction: .personaliseProfile) } label: {
+ Text(VectorL10n.onboardingCongratulationsPersonaliseButton)
+ .font(theme.fonts.bodySB)
+ .foregroundColor(theme.colors.accent)
+ }
+ .buttonStyle(PrimaryActionButtonStyle(customColor: .white))
+
+ Button { viewModel.send(viewAction: .takeMeHome) } label: {
+ Text(VectorL10n.onboardingCongratulationsHomeButton)
+ .font(theme.fonts.body)
+ .padding(.vertical, 12)
+ }
+ }
+ }
+}
+
+// MARK: - Previews
+
+@available(iOS 14.0, *)
+struct OnboardingCongratulationsScreen_Previews: PreviewProvider {
+ static let stateRenderer = MockOnboardingCongratulationsScreenState.stateRenderer
+ static var previews: some View {
+ stateRenderer.screenGroup()
+ }
+}