#106: Add Analytics UI

This commit is contained in:
Doug
2022-09-02 10:49:59 +01:00
committed by Doug
parent df68174a32
commit 8e46d0e3d6
26 changed files with 1437 additions and 85 deletions
+1
View File
@@ -286,6 +286,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
private func presentSettingsScreen() {
let parameters = SettingsCoordinatorParameters(navigationRouter: navigationRouter,
userSession: userSession,
bugReportService: bugReportService)
let coordinator = SettingsCoordinator(parameters: parameters)
coordinator.callback = { [weak self] action in
+2
View File
@@ -23,6 +23,8 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
internal enum Asset {
internal enum Images {
internal static let analyticsCheckmark = ImageAsset(name: "Images/AnalyticsCheckmark")
internal static let analyticsLogo = ImageAsset(name: "Images/AnalyticsLogo")
internal static let serverSelectionIcon = ImageAsset(name: "Images/Server Selection Icon")
internal static let splashScreenPage1 = ImageAsset(name: "Images/Splash Screen Page 1")
internal static let splashScreenPage2 = ImageAsset(name: "Images/Splash Screen Page 2")
@@ -0,0 +1,73 @@
//
// 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 AnalyticsPromptCoordinatorParameters {
/// The user session to use if analytics are enabled.
let userSession: UserSessionProtocol
}
final class AnalyticsPromptCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let parameters: AnalyticsPromptCoordinatorParameters
private let analyticsPromptHostingController: UIViewController
private var analyticsPromptViewModel: AnalyticsPromptViewModel
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var callback: (@MainActor () -> Void)?
// MARK: - Setup
init(parameters: AnalyticsPromptCoordinatorParameters) {
self.parameters = parameters
let viewModel = AnalyticsPromptViewModel(termsURL: BuildSettings.analyticsConfiguration.termsURL)
let view = AnalyticsPrompt(context: viewModel.context)
analyticsPromptViewModel = viewModel
analyticsPromptHostingController = UIHostingController(rootView: view)
}
// MARK: - Public
func start() {
MXLog.debug("Did start.")
analyticsPromptViewModel.callback = { [weak self] result in
MXLog.debug("AnalyticsPromptViewModel did complete with result: \(result).")
guard let self = self else { return }
switch result {
case .enable:
Analytics.shared.optIn(with: self.parameters.userSession)
self.callback?()
case .disable:
Analytics.shared.optOut()
self.callback?()
}
}
}
func toPresentable() -> UIViewController { analyticsPromptHostingController }
}
@@ -0,0 +1,63 @@
//
// 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
enum AnalyticsPromptViewAction {
/// Enable analytics.
case enable
/// Disable analytics.
case disable
}
enum AnalyticsPromptViewModelAction {
/// Enable analytics.
case enable
/// Disable analytics.
case disable
}
struct AnalyticsPromptViewState: BindableState {
/// Attributed strings created from localized HTML.
let strings = AnalyticsPromptStrings()
}
/// A collection of strings for the UI that need to be parsed from HTML
struct AnalyticsPromptStrings {
let optInContent: AttributedString
let point1 = AttributedStringBuilder().fromHTML(ElementL10n.analyticsOptInListItem1) ?? AttributedString(ElementL10n.analyticsOptInListItem1)
let point2 = AttributedStringBuilder().fromHTML(ElementL10n.analyticsOptInListItem2) ?? AttributedString(ElementL10n.analyticsOptInListItem2)
init() {
// Create the opt in content with a placeholder.
let linkPlaceholder = "{link}"
var optInContent = AttributedString(ElementL10n.analyticsOptInContent(ElementInfoPlist.cfBundleName, linkPlaceholder))
guard let range = optInContent.range(of: linkPlaceholder) else {
self.optInContent = AttributedString(ElementL10n.analyticsOptInContent(ElementInfoPlist.cfBundleName,
ElementL10n.analyticsOptInContentLink))
MXLog.failure("Failed to add a link attribute to the opt in content.")
return
}
// Replace the placeholder with a link.
var link = AttributedString(ElementL10n.analyticsOptInContentLink)
link.link = BuildSettings.analyticsConfiguration.termsURL
optInContent.replaceSubrange(range, with: link)
self.optInContent = optInContent
}
}
@@ -0,0 +1,51 @@
//
// 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 Combine
import SwiftUI
typealias AnalyticsPromptViewModelType = StateStoreViewModel<AnalyticsPromptViewState, AnalyticsPromptViewAction>
class AnalyticsPromptViewModel: AnalyticsPromptViewModelType, AnalyticsPromptViewModelProtocol {
// MARK: - Properties
// MARK: Private
private let termsURL: URL
// MARK: Public
var callback: (@MainActor (AnalyticsPromptViewModelAction) -> Void)?
// MARK: - Setup
/// Initialize a view model with the specified prompt type and app display name.
init(termsURL: URL) {
self.termsURL = termsURL
super.init(initialViewState: AnalyticsPromptViewState())
}
// MARK: - Public
override func process(viewAction: AnalyticsPromptViewAction) async {
switch viewAction {
case .enable:
callback?(.enable)
case .disable:
callback?(.disable)
}
}
}
@@ -0,0 +1,23 @@
//
// 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
@MainActor
protocol AnalyticsPromptViewModelProtocol {
var callback: (@MainActor (AnalyticsPromptViewModelAction) -> Void)? { get set }
var context: AnalyticsPromptViewModelType.Context { get }
}
@@ -0,0 +1,130 @@
//
// 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
/// A prompt that asks the user whether they would like to enable Analytics or not.
struct AnalyticsPrompt: View {
// MARK: - Properties
// MARK: Private
private let horizontalPadding: CGFloat = 16
// MARK: Public
@ObservedObject var context: AnalyticsPromptViewModel.Context
// MARK: Views
var body: some View {
GeometryReader { geometry in
ScrollView {
Spacer()
.frame(height: UIConstants.spacerHeight(in: geometry))
mainContent
.readableFrame()
.padding(.horizontal, horizontalPadding)
.padding(.top, UIConstants.onboardingBreakerScreenTopPadding)
.padding(.bottom, 8)
}
.safeAreaInset(edge: .bottom) {
VStack {
buttons
.readableFrame()
.padding(.horizontal, horizontalPadding)
.padding(.bottom, UIConstants.actionButtonBottomPadding)
Spacer()
.frame(height: UIConstants.spacerHeight(in: geometry))
}
.padding(.top, 8)
.background(Color.element.background.ignoresSafeArea())
}
.background(Color.element.background.ignoresSafeArea())
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
/// The main content of the screen that is shown inside the scroll view.
private var mainContent: some View {
VStack {
Image(uiImage: Asset.Images.analyticsLogo.image)
.padding(.bottom, 25)
Text(ElementL10n.analyticsOptInTitle(ElementInfoPlist.cfBundleName))
.font(.element.title2B)
.multilineTextAlignment(.center)
.foregroundColor(.element.primaryContent)
.padding(.bottom, 2)
Text(context.viewState.strings.optInContent)
.font(.element.body)
.multilineTextAlignment(.center)
.foregroundColor(.element.secondaryContent)
Divider()
.background(Color.element.quinaryContent)
.padding(.vertical, 28)
checkmarkList
}
}
/// The list of re-assurances about analytics.
private var checkmarkList: some View {
VStack(alignment: .leading) {
AnalyticsPromptCheckmarkItem(attributedString: context.viewState.strings.point1)
AnalyticsPromptCheckmarkItem(attributedString: context.viewState.strings.point1)
AnalyticsPromptCheckmarkItem(string: ElementL10n.analyticsOptInListItem3)
}
.fixedSize(horizontal: false, vertical: true)
.font(.element.body)
.foregroundColor(.element.secondaryContent)
.frame(maxWidth: .infinity)
}
/// The stack of enable/disable buttons.
private var buttons: some View {
VStack {
Button { context.send(viewAction: .enable) } label: {
Text(ElementL10n.actionEnable)
.font(.element.bodySB)
}
.buttonStyle(.elementAction(.xLarge))
.accessibilityIdentifier("enableButton")
Button { context.send(viewAction: .disable) } label: {
Text(ElementL10n.actionNotNow)
.font(.element.bodySB)
.padding(12)
}
.accessibilityIdentifier("disableButton")
}
}
}
// MARK: - Previews
struct AnalyticsPrompt_Previews: PreviewProvider {
static let viewModel = AnalyticsPromptViewModel(termsURL: BuildSettings.analyticsConfiguration.termsURL)
static var previews: some View {
AnalyticsPrompt(context: viewModel.context)
.tint(.element.accent)
}
}
@@ -0,0 +1,60 @@
//
// 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 AnalyticsPromptCheckmarkItem: View {
// MARK: - Properties
// MARK: Private
private let attributedString: AttributedString
// MARK: - Setup
init(attributedString: AttributedString) {
self.attributedString = attributedString
}
init(string: String) {
attributedString = AttributedString(string)
}
// MARK: - Views
var body: some View {
Label { Text(attributedString) } icon: {
Image(uiImage: Asset.Images.analyticsCheckmark.image)
.foregroundColor(.element.accent)
}
}
}
// MARK: - Previews
struct AnalyticsPromptCheckmarkItem_Previews: PreviewProvider {
static let strings = AnalyticsPromptStrings()
static var previews: some View {
VStack(alignment: .leading) {
AnalyticsPromptCheckmarkItem(attributedString: strings.point1)
AnalyticsPromptCheckmarkItem(attributedString: strings.point2)
AnalyticsPromptCheckmarkItem(attributedString: AttributedString("This is a short string."))
AnalyticsPromptCheckmarkItem(attributedString: AttributedString("This is a very long string that will be used to test the layout over multiple lines of text to ensure everything is correct."))
}
.padding()
}
}
@@ -51,6 +51,7 @@ class AuthenticationCoordinator: Coordinator, Presentable {
// MARK: - Private
/// Shows the splash screen as the root view in the navigation stack.
private func showSplashScreen() {
let coordinator = SplashScreenCoordinator()
@@ -129,6 +130,23 @@ class AuthenticationCoordinator: Coordinator, Presentable {
}
}
private func showAnalyticsPrompt(with userSession: UserSessionProtocol) {
let parameters = AnalyticsPromptCoordinatorParameters(userSession: userSession)
let coordinator = AnalyticsPromptCoordinator(parameters: parameters)
coordinator.callback = { [weak self] in
guard let self = self else { return }
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
}
coordinator.start()
add(childCoordinator: coordinator)
navigationRouter.setRootModule(coordinator, hideNavigationBar: true, animated: true) { [weak self] in
self?.remove(childCoordinator: coordinator)
}
}
/// Show a blocking activity indicator.
private func startLoading() {
activityIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true))
@@ -25,6 +25,9 @@ struct UIConstants {
/// The padding used between the footer and the bottom of the view.
static let actionButtonBottomPadding: CGFloat = 24
/// The padding used to the top of the view for breaker screens that don't have a navigation bar.
static let onboardingBreakerScreenTopPadding: CGFloat = 80
/// The height to use for top/bottom spacers to pad the views to fit the `maxContentHeight`.
static func spacerHeight(in geometry: GeometryProxy) -> CGFloat {
max(0, (geometry.size.height - maxContentHeight) / 2)
@@ -18,6 +18,7 @@ import SwiftUI
struct SettingsCoordinatorParameters {
let navigationRouter: NavigationRouterType
let userSession: UserSessionProtocol
let bugReportService: BugReportServiceProtocol
}
@@ -60,6 +61,8 @@ final class SettingsCoordinator: Coordinator, Presentable {
guard let self = self else { return }
MXLog.debug("SettingsViewModel did complete with result: \(result).")
switch result {
case .toggleAnalytics:
self.toggleAnalytics()
case .reportBug:
self.presentBugReportScreen()
case .crash:
@@ -82,17 +85,12 @@ final class SettingsCoordinator: Coordinator, Presentable {
// MARK: - Private
/// Show an activity indicator whilst loading.
/// - Parameters:
/// - label: The label to show on the indicator.
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
}
/// Hide the currently displayed activity indicator.
private func stopLoading() {
loadingIndicator = nil
private func toggleAnalytics() {
if ElementSettings.shared.enableAnalytics {
Analytics.shared.optOut()
} else {
Analytics.shared.optIn(with: parameters.userSession)
}
}
private func presentBugReportScreen() {
@@ -114,6 +112,19 @@ final class SettingsCoordinator: Coordinator, Presentable {
self.remove(childCoordinator: coordinator)
}
}
/// Show an activity indicator whilst loading.
/// - Parameters:
/// - label: The label to show on the indicator.
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
}
/// Hide the currently displayed activity indicator.
private func stopLoading() {
loadingIndicator = nil
}
/// Show success indicator
private func showSuccess(label: String) {
@@ -21,6 +21,7 @@ import Foundation
// MARK: View model
enum SettingsViewModelAction {
case toggleAnalytics
case reportBug
case crash
case logout
@@ -32,9 +33,12 @@ struct SettingsViewState: BindableState {
var bindings: SettingsViewStateBindings
}
struct SettingsViewStateBindings { }
struct SettingsViewStateBindings {
var enableAnalytics = ElementSettings.shared.enableAnalytics
}
enum SettingsViewAction {
case toggleAnalytics
case reportBug
case crash
case logout
@@ -40,6 +40,8 @@ class SettingsViewModel: SettingsViewModelType, SettingsViewModelProtocol {
override func process(viewAction: SettingsViewAction) async {
switch viewAction {
case .toggleAnalytics:
callback?(.toggleAnalytics)
case .reportBug:
callback?(.reportBug)
case .crash:
@@ -31,43 +31,14 @@ struct SettingsScreen: View {
var body: some View {
Form {
Section {
Button { context.send(viewAction: .reportBug) } label: {
Text(ElementL10n.sendBugReport)
}
.foregroundColor(Color.element.primaryContent)
.accessibilityIdentifier("reportBugButton")
if BuildSettings.settingsCrashButtonVisible {
Button("Crash the app",
role: .destructive) { context.send(viewAction: .crash)
}
.accessibilityIdentifier("crashButton")
}
}
.listRowBackground(rowBackgroundColor)
analyticsSection
.listRowBackground(rowBackgroundColor)
userInterfaceSection
.listRowBackground(rowBackgroundColor)
Section {
Button { showingLogoutConfirmation = true } label: {
Text(ElementL10n.actionSignOut)
}
.frame(maxWidth: .infinity)
.foregroundColor(Color.element.primaryContent)
.accessibilityIdentifier("logoutButton")
.confirmationDialog(ElementL10n.actionSignOutConfirmationSimple,
isPresented: $showingLogoutConfirmation,
titleVisibility: .visible) {
Button(ElementL10n.actionSignOut,
role: .destructive) { context.send(viewAction: .logout)
}
}
} footer: {
versionText
}
.listRowBackground(rowBackgroundColor)
logoutSection
.listRowBackground(rowBackgroundColor)
}
.introspectTableView { tableView in
tableView.backgroundColor = .clear
@@ -87,11 +58,28 @@ struct SettingsScreen: View {
private var rowBackgroundColor: Color {
colorScheme == .light ? .element.background : .element.system
}
private var analyticsSection: some View {
Section(ElementL10n.settingsAnalytics) {
Button { context.send(viewAction: .reportBug) } label: {
Text(ElementL10n.sendBugReport)
}
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier("reportBugButton")
if BuildSettings.settingsCrashButtonVisible {
Button("Crash the app",
role: .destructive) { context.send(viewAction: .crash)
}
.accessibilityIdentifier("crashButton")
}
}
}
@ViewBuilder
private var userInterfaceSection: some View {
if BuildSettings.settingsShowTimelineStyle {
Section(header: Text(ElementL10n.settingsUserInterface)) {
Section(ElementL10n.settingsUserInterface) {
Picker(ElementL10n.settingsTimelineStyle, selection: $settings.timelineStyle) {
ForEach(TimelineStyle.allCases, id: \.self) { style in
Text(style.description)
@@ -100,7 +88,26 @@ struct SettingsScreen: View {
}
.accessibilityIdentifier("timelineStylePicker")
}
.listRowBackground(rowBackgroundColor)
}
}
private var logoutSection: some View {
Section {
Button { showingLogoutConfirmation = true } label: {
Text(ElementL10n.actionSignOut)
}
.frame(maxWidth: .infinity)
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier("logoutButton")
.confirmationDialog(ElementL10n.actionSignOutConfirmationSimple,
isPresented: $showingLogoutConfirmation,
titleVisibility: .visible) {
Button(ElementL10n.actionSignOut,
role: .destructive) { context.send(viewAction: .logout)
}
}
} footer: {
versionText
}
}
}
@@ -56,14 +56,14 @@ class Analytics {
// MARK: - Public
/// Opts in to analytics tracking with the supplied session.
/// - Parameter session: An optional session to use to when reading/generating the analytics ID.
/// Opts in to analytics tracking with the supplied user session.
/// - Parameter userSession: The user session to use to when reading/generating the analytics ID.
/// The session will be ignored if not running.
func optIn(with session: UserSessionProtocol) {
func optIn(with userSession: UserSessionProtocol) {
ElementSettings.shared.enableAnalytics = true
startIfEnabled()
Task { await useAnalyticsSettings(from: session) }
Task { await useAnalyticsSettings(from: userSession) }
}
/// Stops analytics tracking and calls `reset` to clear any IDs and event queues.
@@ -95,16 +95,16 @@ class Analytics {
// MXLogger.setBuildVersion(ElementInfoPlist.cfBundleShortVersionString)
}
/// Use the analytics settings from the supplied session to configure analytics.
/// Use the analytics settings from the supplied user session to configure analytics.
/// For now this is only used for (pseudonymous) identification.
/// - Parameter session: The session to read analytics settings from.
func useAnalyticsSettings(from session: UserSessionProtocol) async {
/// - Parameter userSession: The user session to read analytics settings from.
func useAnalyticsSettings(from userSession: UserSessionProtocol) async {
guard
ElementSettings.shared.enableAnalytics,
!ElementSettings.shared.isIdentifiedForAnalytics
else { return }
let service = AnalyticsService(session: session)
let service = AnalyticsService(userSession: userSession)
self.service = service
switch await service.settings() {
@@ -17,7 +17,7 @@
import Foundation
enum AnalyticsServiceError: Error {
/// The session supplied to the service does not have a state of `MXSessionStateRunning`.
/// The user session supplied to the service does not have a state of `MXSessionStateRunning`.
case sessionIsNotRunning
/// The service failed to get or update the analytics settings event from the user's account data.
case accountDataFailure
@@ -25,12 +25,12 @@ enum AnalyticsServiceError: Error {
/// A service responsible for handling the `im.vector.analytics` event from the user's account data.
class AnalyticsService {
let session: UserSessionProtocol
let userSession: UserSessionProtocol
/// Creates an analytics service with the supplied session.
/// - Parameter session: The session to use when reading analytics settings from account data.
init(session: UserSessionProtocol) {
self.session = session
/// Creates an analytics service with the supplied user session.
/// - Parameter userSession: The user session to use when reading analytics settings from account data.
init(userSession: UserSessionProtocol) {
self.userSession = userSession
}
/// The analytics settings for the current user. Calling this method will check whether the settings already
@@ -41,12 +41,12 @@ class AnalyticsService {
func settings() async -> Result<AnalyticsSettings, AnalyticsServiceError> {
// Only use the session if it is running otherwise we could wipe out an existing analytics ID.
fatalWithoutUnreachableCodeWarning()
// guard session.state == .running else {
// guard userSession.state == .running else {
// MXLog.warning("Aborting attempt to read analytics settings. The session may not be up-to-date.")
// return .failure(.sessionIsNotRunning)
// }
let result: Result<AnalyticsSettings?, ClientProxyError> = await session.clientProxy.accountDataEvent(type: AnalyticsSettings.eventType)
let result: Result<AnalyticsSettings?, ClientProxyError> = await userSession.clientProxy.accountDataEvent(type: AnalyticsSettings.eventType)
switch result {
case .failure:
return .failure(.accountDataFailure)
@@ -57,7 +57,7 @@ class AnalyticsService {
}
let newSettings = AnalyticsSettings.new(currentEvent: settings)
switch await session.clientProxy.setAccountData(content: newSettings, type: AnalyticsSettings.eventType) {
switch await userSession.clientProxy.setAccountData(content: newSettings, type: AnalyticsSettings.eventType) {
case .failure:
MXLog.warning("Failed to update analytics settings.")
return .failure(.accountDataFailure)
@@ -21,6 +21,7 @@ enum UITestScreenIdentifier: String {
case serverSelection
case serverSelectionNonModal
case authenticationFlow
case analyticsPrompt
case simpleRegular
case simpleUpgrade
case settings
@@ -76,6 +76,9 @@ class MockScreen: Identifiable {
case .serverSelectionNonModal:
return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(),
hasModalPresentation: false))
case .analyticsPrompt:
return AnalyticsPromptCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),
mediaProvider: MockMediaProvider())))
case .authenticationFlow:
return AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
navigationRouter: navigationRouter)
@@ -85,6 +88,8 @@ class MockScreen: Identifiable {
return TemplateCoordinator(parameters: .init(promptType: .upgrade))
case .settings:
return SettingsCoordinator(parameters: .init(navigationRouter: navigationRouter,
userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),
mediaProvider: MockMediaProvider()),
bugReportService: MockBugReportService()))
case .bugReport:
return BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(),