diff --git a/Dangerfile.swift b/Dangerfile.swift
index 85e4692fb..6fa7301f3 100644
--- a/Dangerfile.swift
+++ b/Dangerfile.swift
@@ -52,7 +52,8 @@ let allowList = ["stefanceriu",
"aringenbach",
"flescio",
"Velin92",
- "alfogrillo"]
+ "alfogrillo",
+ "nimau"]
let requiresSignOff = !allowList.contains(where: {
$0.caseInsensitiveCompare(danger.github.pullRequest.user.login) == .orderedSame
diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings
index 5a00e4990..404695a7c 100644
--- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings
+++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings
@@ -53,6 +53,7 @@
"action_view_source" = "View Source";
"action_yes" = "Yes";
"common_about" = "About";
+"common_analytics" = "Analytics";
"common_audio" = "Audio";
"common_bubbles" = "Bubbles";
"common_creating_room" = "Creating room…";
@@ -161,6 +162,17 @@
"room_timeline_beginning_of_room" = "This is the beginning of %1$@.";
"room_timeline_beginning_of_room_no_name" = "This is the beginning of this conversation.";
"room_timeline_read_marker_title" = "New";
+"screen_analytics_help_us_improve" = "Help us identify issues and improve %1$@ by sharing anonymous usage data.";
+"screen_analytics_prompt_data_usage" = "We don't record or profile any account data";
+"screen_analytics_prompt_help_us_improve" = "Help us identify issues and improve %1$@ by sharing anonymous usage data.";
+"screen_analytics_prompt_read_terms" = "You can read all our terms %1$@.";
+"screen_analytics_prompt_read_terms_content_link" = "here";
+"screen_analytics_prompt_settings" = "You can turn this off anytime in settings";
+"screen_analytics_prompt_third_party_sharing" = "We don't share information with third parties";
+"screen_analytics_prompt_title" = "Help improve %1$@";
+"screen_analytics_read_terms" = "You can read all our terms %1$@.";
+"screen_analytics_read_terms_content_link" = "here";
+"screen_analytics_share_data" = "Share analytics data";
"screen_bug_report_attach_screenshot" = "Attach screenshot";
"screen_bug_report_contact_me" = "You may contact me if you have any follow up questions";
"screen_bug_report_edit_screenshot" = "Edit screenshot";
@@ -198,6 +210,10 @@
"screen_dm_details_unblock_alert_action" = "Unblock";
"screen_dm_details_unblock_alert_description" = "On unblocking the user, you will be able to see all messages by them again.";
"screen_dm_details_unblock_user" = "Unblock user";
+"screen_invites_decline_chat_message" = "Are you sure you want to decline joining %1$@?";
+"screen_invites_decline_chat_title" = "Decline invite";
+"screen_invites_decline_direct_chat_message" = "Are you sure you want to decline to chat with %1$@?";
+"screen_invites_decline_direct_chat_title" = "Decline chat";
"screen_invites_empty_list" = "No Invites";
"screen_invites_invited_you" = "%1$@ invited you";
"screen_login_error_deactivated_account" = "This account has been deactivated.";
diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings
index 7b4d02329..6bfab1e5d 100644
--- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings
+++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings
@@ -4,15 +4,6 @@
/* Used for testing */
"untranslated" = "Untranslated";
-// MARK: - Analytics
-
-"analytics_opt_in_title" = "Help improve %@";
-"analytics_opt_in_content" = "Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices.\n\nYou can read all our terms %@.";
-"analytics_opt_in_content_link" = "here";
-"analytics_opt_in_list_item_1" = "We don\'t record or profile any account data";
-"analytics_opt_in_list_item_2" = "We don\'t share information with third parties";
-"analytics_opt_in_list_item_3" = "You can turn this off anytime in settings";
-
// MARK: - Soft logout
"soft_logout_forgot_password" = "Forgot password";
diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift
index f123a5d1b..f35a73a0c 100644
--- a/ElementX/Sources/Application/AppCoordinator.swift
+++ b/ElementX/Sources/Application/AppCoordinator.swift
@@ -42,7 +42,6 @@ class AppCoordinator: AppCoordinatorProtocol {
private var userSessionFlowCoordinator: UserSessionFlowCoordinator?
private var authenticationCoordinator: AuthenticationCoordinator?
- private let bugReportService: BugReportServiceProtocol
private let backgroundTaskService: BackgroundTaskServiceProtocol
private var userSessionCancellables = Set()
@@ -56,11 +55,11 @@ class AppCoordinator: AppCoordinatorProtocol {
Self.setupServiceLocator(navigationRootCoordinator: navigationRootCoordinator)
Self.setupLogging()
-
- stateMachine = AppCoordinatorStateMachine()
-
- bugReportService = BugReportService(withBaseURL: ServiceLocator.shared.settings.bugReportServiceBaseURL, sentryURL: ServiceLocator.shared.settings.bugReportSentryURL)
+ ServiceLocator.shared.analytics.startIfEnabled()
+
+ stateMachine = AppCoordinatorStateMachine()
+
navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator())
backgroundTaskService = UIKitBackgroundTaskService {
@@ -84,7 +83,7 @@ class AppCoordinator: AppCoordinatorProtocol {
wipeUserData(includingSettings: true)
}
ServiceLocator.shared.settings.lastVersionLaunched = currentVersion.description
-
+
setupStateMachine()
observeApplicationState()
@@ -114,6 +113,9 @@ class AppCoordinator: AppCoordinatorProtocol {
ServiceLocator.shared.register(userIndicatorController: UserIndicatorController(rootCoordinator: navigationRootCoordinator))
ServiceLocator.shared.register(appSettings: AppSettings())
ServiceLocator.shared.register(networkMonitor: NetworkMonitor())
+ ServiceLocator.shared.register(bugReportService: BugReportService(withBaseURL: ServiceLocator.shared.settings.bugReportServiceBaseURL,
+ sentryURL: ServiceLocator.shared.settings.bugReportSentryURL))
+ ServiceLocator.shared.register(analytics: Analytics(client: PostHogAnalyticsClient()))
}
private static func setupLogging() {
@@ -248,7 +250,7 @@ class AppCoordinator: AppCoordinatorProtocol {
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SplashScreenCoordinator())
let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession,
navigationSplitCoordinator: navigationSplitCoordinator,
- bugReportService: bugReportService,
+ bugReportService: ServiceLocator.shared.bugReportService,
roomTimelineControllerFactory: RoomTimelineControllerFactory())
userSessionFlowCoordinator.callback = { [weak self] action in
@@ -292,6 +294,9 @@ class AppCoordinator: AppCoordinatorProtocol {
userSessionStore.logout(userSession: userSession)
tearDownUserSession()
+ // reset analytics
+ ServiceLocator.shared.analytics.reset()
+
stateMachine.processEvent(.completedSigningOut(isSoft: isSoft))
}
}
diff --git a/ElementX/Sources/Application/AppCoordinatorStateMachine.swift b/ElementX/Sources/Application/AppCoordinatorStateMachine.swift
index 84a5fb08f..7183d14d5 100644
--- a/ElementX/Sources/Application/AppCoordinatorStateMachine.swift
+++ b/ElementX/Sources/Application/AppCoordinatorStateMachine.swift
@@ -68,7 +68,6 @@ class AppCoordinatorStateMachine {
private func configure() {
stateMachine.addRoutes(event: .startWithAuthentication, transitions: [.initial => .signedOut])
stateMachine.addRoutes(event: .createdUserSession, transitions: [.signedOut => .signedIn])
-
stateMachine.addRoutes(event: .startWithExistingSession, transitions: [.initial => .restoringSession])
stateMachine.addRoutes(event: .createdUserSession, transitions: [.restoringSession => .signedIn])
stateMachine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])
diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift
index c7b4d43e8..8fe678b30 100644
--- a/ElementX/Sources/Application/AppSettings.swift
+++ b/ElementX/Sources/Application/AppSettings.swift
@@ -23,7 +23,6 @@ final class AppSettings: ObservableObject {
case lastVersionLaunched
case timelineStyle
case enableAnalytics
- case isIdentifiedForAnalytics
case enableInAppNotifications
case pusherProfileTag
case shouldCollapseRoomStateEvents
@@ -104,7 +103,7 @@ final class AppSettings: ObservableObject {
let bugReportUISIId = "element-auto-uisi"
// MARK: - Analytics
-
+
#if DEBUG
/// The configuration to use for analytics during development. Set `isEnabled` to false to disable analytics in debug builds.
/// **Note:** Analytics are disabled by default for forks. If you are maintaining a fork, set custom configurations.
@@ -129,13 +128,7 @@ final class AppSettings: ObservableObject {
/// `true` when the user has opted in to send analytics.
@UserSetting(key: UserDefaultsKeys.enableAnalytics.rawValue, defaultValue: false, persistIn: store)
var enableAnalytics
-
- /// Indicates if the device has already called identify for this session to PostHog.
- /// This is separate to `enableAnalytics` as logging out leaves analytics
- /// enabled, but requires the next account to be identified separately.
- @UserSetting(key: UserDefaultsKeys.isIdentifiedForAnalytics.rawValue, defaultValue: false, persistIn: store)
- var isIdentifiedForAnalytics
-
+
// MARK: - Room Screen
@UserSettingRawRepresentable(key: UserDefaultsKeys.timelineStyle.rawValue, defaultValue: TimelineStyle.bubbles, persistIn: store)
diff --git a/ElementX/Sources/Application/ServiceLocator.swift b/ElementX/Sources/Application/ServiceLocator.swift
index 2a674c62d..e31cdcb32 100644
--- a/ElementX/Sources/Application/ServiceLocator.swift
+++ b/ElementX/Sources/Application/ServiceLocator.swift
@@ -38,4 +38,16 @@ class ServiceLocator {
func register(networkMonitor: NetworkMonitor) {
self.networkMonitor = networkMonitor
}
+
+ private(set) var analytics: Analytics!
+
+ func register(analytics: Analytics) {
+ self.analytics = analytics
+ }
+
+ private(set) var bugReportService: BugReportServiceProtocol!
+
+ func register(bugReportService: BugReportServiceProtocol) {
+ self.bugReportService = bugReportService
+ }
}
diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift
index 3d26c0517..ec4a4eca2 100644
--- a/ElementX/Sources/Generated/Strings+Untranslated.swift
+++ b/ElementX/Sources/Generated/Strings+Untranslated.swift
@@ -10,24 +10,6 @@ import Foundation
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
public enum UntranslatedL10n {
- /// Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices.
- ///
- /// You can read all our terms %@.
- public static func analyticsOptInContent(_ p1: Any, _ p2: Any) -> String {
- return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_content", String(describing: p1), String(describing: p2))
- }
- /// here
- public static var analyticsOptInContentLink: String { return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_content_link") }
- /// We don't record or profile any account data
- public static var analyticsOptInListItem1: String { return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_list_item_1") }
- /// We don't share information with third parties
- public static var analyticsOptInListItem2: String { return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_list_item_2") }
- /// You can turn this off anytime in settings
- public static var analyticsOptInListItem3: String { return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_list_item_3") }
- /// Help improve %@
- public static func analyticsOptInTitle(_ p1: Any) -> String {
- return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_title", String(describing: p1))
- }
/// Camera
public static var mediaUploadCameraPicker: String { return UntranslatedL10n.tr("Untranslated", "media_upload_camera_picker") }
/// Document
diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift
index 063d0a6ad..f4639ed94 100644
--- a/ElementX/Sources/Generated/Strings.swift
+++ b/ElementX/Sources/Generated/Strings.swift
@@ -120,6 +120,8 @@ public enum L10n {
public static var actionYes: String { return L10n.tr("Localizable", "action_yes") }
/// About
public static var commonAbout: String { return L10n.tr("Localizable", "common_about") }
+ /// Analytics
+ public static var commonAnalytics: String { return L10n.tr("Localizable", "common_analytics") }
/// Audio
public static var commonAudio: String { return L10n.tr("Localizable", "common_audio") }
/// Bubbles
@@ -390,6 +392,38 @@ public enum L10n {
public static func roomTimelineStateChanges(_ p1: Int) -> String {
return L10n.tr("Localizable", "room_timeline_state_changes", p1)
}
+ /// Help us identify issues and improve %1$@ by sharing anonymous usage data.
+ public static func screenAnalyticsHelpUsImprove(_ p1: Any) -> String {
+ return L10n.tr("Localizable", "screen_analytics_help_us_improve", String(describing: p1))
+ }
+ /// We don't record or profile any account data
+ public static var screenAnalyticsPromptDataUsage: String { return L10n.tr("Localizable", "screen_analytics_prompt_data_usage") }
+ /// Help us identify issues and improve %1$@ by sharing anonymous usage data.
+ public static func screenAnalyticsPromptHelpUsImprove(_ p1: Any) -> String {
+ return L10n.tr("Localizable", "screen_analytics_prompt_help_us_improve", String(describing: p1))
+ }
+ /// You can read all our terms %1$@.
+ public static func screenAnalyticsPromptReadTerms(_ p1: Any) -> String {
+ return L10n.tr("Localizable", "screen_analytics_prompt_read_terms", String(describing: p1))
+ }
+ /// here
+ public static var screenAnalyticsPromptReadTermsContentLink: String { return L10n.tr("Localizable", "screen_analytics_prompt_read_terms_content_link") }
+ /// You can turn this off anytime in settings
+ public static var screenAnalyticsPromptSettings: String { return L10n.tr("Localizable", "screen_analytics_prompt_settings") }
+ /// We don't share information with third parties
+ public static var screenAnalyticsPromptThirdPartySharing: String { return L10n.tr("Localizable", "screen_analytics_prompt_third_party_sharing") }
+ /// Help improve %1$@
+ public static func screenAnalyticsPromptTitle(_ p1: Any) -> String {
+ return L10n.tr("Localizable", "screen_analytics_prompt_title", String(describing: p1))
+ }
+ /// You can read all our terms %1$@.
+ public static func screenAnalyticsReadTerms(_ p1: Any) -> String {
+ return L10n.tr("Localizable", "screen_analytics_read_terms", String(describing: p1))
+ }
+ /// here
+ public static var screenAnalyticsReadTermsContentLink: String { return L10n.tr("Localizable", "screen_analytics_read_terms_content_link") }
+ /// Share analytics data
+ public static var screenAnalyticsShareData: String { return L10n.tr("Localizable", "screen_analytics_share_data") }
/// Attach screenshot
public static var screenBugReportAttachScreenshot: String { return L10n.tr("Localizable", "screen_bug_report_attach_screenshot") }
/// You may contact me if you have any follow up questions
@@ -468,6 +502,18 @@ public enum L10n {
public static var screenDmDetailsUnblockAlertDescription: String { return L10n.tr("Localizable", "screen_dm_details_unblock_alert_description") }
/// Unblock user
public static var screenDmDetailsUnblockUser: String { return L10n.tr("Localizable", "screen_dm_details_unblock_user") }
+ /// Are you sure you want to decline joining %1$@?
+ public static func screenInvitesDeclineChatMessage(_ p1: Any) -> String {
+ return L10n.tr("Localizable", "screen_invites_decline_chat_message", String(describing: p1))
+ }
+ /// Decline invite
+ public static var screenInvitesDeclineChatTitle: String { return L10n.tr("Localizable", "screen_invites_decline_chat_title") }
+ /// Are you sure you want to decline to chat with %1$@?
+ public static func screenInvitesDeclineDirectChatMessage(_ p1: Any) -> String {
+ return L10n.tr("Localizable", "screen_invites_decline_direct_chat_message", String(describing: p1))
+ }
+ /// Decline chat
+ public static var screenInvitesDeclineDirectChatTitle: String { return L10n.tr("Localizable", "screen_invites_decline_direct_chat_title") }
/// No Invites
public static var screenInvitesEmptyList: String { return L10n.tr("Localizable", "screen_invites_empty_list") }
/// %1$@ invited you
diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
index 2f1c08a56..6ff7f786c 100644
--- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
+++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
@@ -5,13 +5,159 @@
import Combine
import Foundation
import MatrixRustSDK
+import AnalyticsEvents
+class AnalyticsClientMock: AnalyticsClientProtocol {
+ var isRunning: Bool {
+ get { return underlyingIsRunning }
+ set(value) { underlyingIsRunning = value }
+ }
+ var underlyingIsRunning: Bool!
+
+ //MARK: - start
+
+ var startCallsCount = 0
+ var startCalled: Bool {
+ return startCallsCount > 0
+ }
+ var startClosure: (() -> Void)?
+
+ func start() {
+ startCallsCount += 1
+ startClosure?()
+ }
+ //MARK: - reset
+
+ var resetCallsCount = 0
+ var resetCalled: Bool {
+ return resetCallsCount > 0
+ }
+ var resetClosure: (() -> Void)?
+
+ func reset() {
+ resetCallsCount += 1
+ resetClosure?()
+ }
+ //MARK: - stop
+
+ var stopCallsCount = 0
+ var stopCalled: Bool {
+ return stopCallsCount > 0
+ }
+ var stopClosure: (() -> Void)?
+
+ func stop() {
+ stopCallsCount += 1
+ stopClosure?()
+ }
+ //MARK: - flush
+
+ var flushCallsCount = 0
+ var flushCalled: Bool {
+ return flushCallsCount > 0
+ }
+ var flushClosure: (() -> Void)?
+
+ func flush() {
+ flushCallsCount += 1
+ flushClosure?()
+ }
+ //MARK: - capture
+
+ var captureCallsCount = 0
+ var captureCalled: Bool {
+ return captureCallsCount > 0
+ }
+ var captureReceivedEvent: AnalyticsEventProtocol?
+ var captureReceivedInvocations: [AnalyticsEventProtocol] = []
+ var captureClosure: ((AnalyticsEventProtocol) -> Void)?
+
+ func capture(_ event: AnalyticsEventProtocol) {
+ captureCallsCount += 1
+ captureReceivedEvent = event
+ captureReceivedInvocations.append(event)
+ captureClosure?(event)
+ }
+ //MARK: - screen
+
+ var screenCallsCount = 0
+ var screenCalled: Bool {
+ return screenCallsCount > 0
+ }
+ var screenReceivedEvent: AnalyticsScreenProtocol?
+ var screenReceivedInvocations: [AnalyticsScreenProtocol] = []
+ var screenClosure: ((AnalyticsScreenProtocol) -> Void)?
+
+ func screen(_ event: AnalyticsScreenProtocol) {
+ screenCallsCount += 1
+ screenReceivedEvent = event
+ screenReceivedInvocations.append(event)
+ screenClosure?(event)
+ }
+ //MARK: - updateUserProperties
+
+ var updateUserPropertiesCallsCount = 0
+ var updateUserPropertiesCalled: Bool {
+ return updateUserPropertiesCallsCount > 0
+ }
+ var updateUserPropertiesReceivedUserProperties: AnalyticsEvent.UserProperties?
+ var updateUserPropertiesReceivedInvocations: [AnalyticsEvent.UserProperties] = []
+ var updateUserPropertiesClosure: ((AnalyticsEvent.UserProperties) -> Void)?
+
+ func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties) {
+ updateUserPropertiesCallsCount += 1
+ updateUserPropertiesReceivedUserProperties = userProperties
+ updateUserPropertiesReceivedInvocations.append(userProperties)
+ updateUserPropertiesClosure?(userProperties)
+ }
+}
class BugReportServiceMock: BugReportServiceProtocol {
+ var isRunning: Bool {
+ get { return underlyingIsRunning }
+ set(value) { underlyingIsRunning = value }
+ }
+ var underlyingIsRunning: Bool!
var crashedLastRun: Bool {
get { return underlyingCrashedLastRun }
set(value) { underlyingCrashedLastRun = value }
}
var underlyingCrashedLastRun: Bool!
+ //MARK: - start
+
+ var startCallsCount = 0
+ var startCalled: Bool {
+ return startCallsCount > 0
+ }
+ var startClosure: (() -> Void)?
+
+ func start() {
+ startCallsCount += 1
+ startClosure?()
+ }
+ //MARK: - stop
+
+ var stopCallsCount = 0
+ var stopCalled: Bool {
+ return stopCallsCount > 0
+ }
+ var stopClosure: (() -> Void)?
+
+ func stop() {
+ stopCallsCount += 1
+ stopClosure?()
+ }
+ //MARK: - reset
+
+ var resetCallsCount = 0
+ var resetCalled: Bool {
+ return resetCallsCount > 0
+ }
+ var resetClosure: (() -> Void)?
+
+ func reset() {
+ resetCallsCount += 1
+ resetClosure?()
+ }
//MARK: - crash
var crashCallsCount = 0
diff --git a/ElementX/Sources/Other/Analytics/ScreenTrackerViewModifier.swift b/ElementX/Sources/Other/Analytics/ScreenTrackerViewModifier.swift
new file mode 100644
index 000000000..f23e49bdc
--- /dev/null
+++ b/ElementX/Sources/Other/Analytics/ScreenTrackerViewModifier.swift
@@ -0,0 +1,36 @@
+//
+// 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
+
+/// `ScreenTrackerViewModifier` is a helper class used to track PostHog screen from SwiftUI screens.
+struct ScreenTrackerViewModifier: ViewModifier {
+ let screen: AnalyticsScreen
+
+ @ViewBuilder
+ func body(content: Content) -> some View {
+ content
+ .onAppear {
+ ServiceLocator.shared.analytics.track(screen: screen)
+ }
+ }
+}
+
+extension View {
+ func track(screen: AnalyticsScreen) -> some View {
+ modifier(ScreenTrackerViewModifier(screen: screen))
+ }
+}
diff --git a/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptCoordinator.swift b/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptCoordinator.swift
index 9c7562468..b728a47a8 100644
--- a/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptCoordinator.swift
+++ b/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptCoordinator.swift
@@ -16,21 +16,13 @@
import SwiftUI
-struct AnalyticsPromptCoordinatorParameters {
- /// The user session to use if analytics are enabled.
- let userSession: UserSessionProtocol
-}
-
final class AnalyticsPromptCoordinator: CoordinatorProtocol {
- private let parameters: AnalyticsPromptCoordinatorParameters
private var viewModel: AnalyticsPromptViewModel
var callback: (@MainActor () -> Void)?
- init(parameters: AnalyticsPromptCoordinatorParameters) {
- self.parameters = parameters
-
- viewModel = AnalyticsPromptViewModel(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
+ init() {
+ viewModel = AnalyticsPromptViewModel()
}
// MARK: - Public
@@ -42,11 +34,11 @@ final class AnalyticsPromptCoordinator: CoordinatorProtocol {
switch result {
case .enable:
MXLog.info("Enable Analytics")
- Analytics.shared.optIn(with: self.parameters.userSession)
+ ServiceLocator.shared.analytics.optIn()
self.callback?()
case .disable:
MXLog.info("Disable Analytics")
- Analytics.shared.optOut()
+ ServiceLocator.shared.analytics.optOut()
self.callback?()
}
}
diff --git a/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptModels.swift b/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptModels.swift
index 982115645..cd709a9d1 100644
--- a/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptModels.swift
+++ b/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptModels.swift
@@ -32,22 +32,24 @@ enum AnalyticsPromptViewModelAction {
struct AnalyticsPromptViewState: BindableState {
/// Attributed strings created from localized HTML.
- let strings = AnalyticsPromptStrings()
+ 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(UntranslatedL10n.analyticsOptInListItem1) ?? AttributedString(UntranslatedL10n.analyticsOptInListItem1)
- let point2 = AttributedStringBuilder().fromHTML(UntranslatedL10n.analyticsOptInListItem2) ?? AttributedString(UntranslatedL10n.analyticsOptInListItem2)
+ let point1 = AttributedStringBuilder().fromHTML(L10n.screenAnalyticsPromptDataUsage) ?? AttributedString(L10n.screenAnalyticsPromptDataUsage)
+ let point2 = AttributedStringBuilder().fromHTML(L10n.screenAnalyticsPromptThirdPartySharing) ?? AttributedString(L10n.screenAnalyticsPromptThirdPartySharing)
+ let point3 = L10n.screenAnalyticsPromptSettings
- init() {
+ init(termsURL: URL) {
+ let content = AttributedString(L10n.screenAnalyticsPromptHelpUsImprove(InfoPlistReader.main.bundleDisplayName))
// Create the opt in content with a placeholder.
let linkPlaceholder = "{link}"
- var optInContent = AttributedString(UntranslatedL10n.analyticsOptInContent(InfoPlistReader.main.bundleDisplayName, linkPlaceholder))
- optInContent.replace(linkPlaceholder,
- with: UntranslatedL10n.analyticsOptInContentLink,
- asLinkTo: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
- self.optInContent = optInContent
+ var readTerms = AttributedString(L10n.screenAnalyticsPromptReadTerms(linkPlaceholder))
+ readTerms.replace(linkPlaceholder,
+ with: L10n.screenAnalyticsPromptReadTermsContentLink,
+ asLinkTo: termsURL)
+ optInContent = content + "\n\n" + readTerms
}
}
diff --git a/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptViewModel.swift b/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptViewModel.swift
index cf30d804a..4267de849 100644
--- a/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptViewModel.swift
+++ b/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptViewModel.swift
@@ -20,14 +20,12 @@ import SwiftUI
typealias AnalyticsPromptViewModelType = StateStoreViewModel
class AnalyticsPromptViewModel: AnalyticsPromptViewModelType, AnalyticsPromptViewModelProtocol {
- private let termsURL: URL
-
var callback: (@MainActor (AnalyticsPromptViewModelAction) -> Void)?
/// Initialize a view model with the specified prompt type and app display name.
- init(termsURL: URL) {
- self.termsURL = termsURL
- super.init(initialViewState: AnalyticsPromptViewState())
+ init() {
+ let promptStrings = AnalyticsPromptStrings(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
+ super.init(initialViewState: AnalyticsPromptViewState(strings: promptStrings))
}
// MARK: - Public
diff --git a/ElementX/Sources/Screens/AnalyticsPrompt/View/AnalyticsPrompt.swift b/ElementX/Sources/Screens/AnalyticsPrompt/View/AnalyticsPrompt.swift
index 7646c9a3d..0079bbcb7 100644
--- a/ElementX/Sources/Screens/AnalyticsPrompt/View/AnalyticsPrompt.swift
+++ b/ElementX/Sources/Screens/AnalyticsPrompt/View/AnalyticsPrompt.swift
@@ -57,9 +57,9 @@ struct AnalyticsPrompt: View {
private var mainContent: some View {
VStack {
Image(uiImage: Asset.Images.analyticsLogo.image)
- .padding(.bottom, 25)
+ .padding(.bottom, 24)
- Text(UntranslatedL10n.analyticsOptInTitle(InfoPlistReader.main.bundleDisplayName))
+ Text(L10n.screenAnalyticsPromptTitle(InfoPlistReader.main.bundleDisplayName))
.font(.element.title2Bold)
.multilineTextAlignment(.center)
.foregroundColor(.element.primaryContent)
@@ -73,7 +73,7 @@ struct AnalyticsPrompt: View {
Divider()
.background(Color.element.quinaryContent)
- .padding(.vertical, 28)
+ .padding(.vertical, 20)
checkmarkList
}
@@ -81,10 +81,10 @@ struct AnalyticsPrompt: View {
/// The list of re-assurances about analytics.
private var checkmarkList: some View {
- VStack(alignment: .leading) {
+ VStack(alignment: .leading, spacing: 8) {
AnalyticsPromptCheckmarkItem(attributedString: context.viewState.strings.point1)
- AnalyticsPromptCheckmarkItem(attributedString: context.viewState.strings.point1)
- AnalyticsPromptCheckmarkItem(string: UntranslatedL10n.analyticsOptInListItem3)
+ AnalyticsPromptCheckmarkItem(attributedString: context.viewState.strings.point2)
+ AnalyticsPromptCheckmarkItem(string: context.viewState.strings.point3)
}
.fixedSize(horizontal: false, vertical: true)
.font(.element.body)
@@ -113,7 +113,7 @@ struct AnalyticsPrompt: View {
// MARK: - Previews
struct AnalyticsPrompt_Previews: PreviewProvider {
- static let viewModel = AnalyticsPromptViewModel(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
+ static let viewModel = AnalyticsPromptViewModel()
static var previews: some View {
AnalyticsPrompt(context: viewModel.context)
}
diff --git a/ElementX/Sources/Screens/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift b/ElementX/Sources/Screens/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift
index a124cf917..e90f06458 100644
--- a/ElementX/Sources/Screens/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift
+++ b/ElementX/Sources/Screens/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift
@@ -29,7 +29,7 @@ struct AnalyticsPromptCheckmarkItem: View {
var body: some View {
Label { Text(attributedString) } icon: {
- Image(uiImage: Asset.Images.analyticsCheckmark.image)
+ Image(systemName: "checkmark.circle")
.foregroundColor(.element.accent)
}
}
@@ -38,7 +38,7 @@ struct AnalyticsPromptCheckmarkItem: View {
// MARK: - Previews
struct AnalyticsPromptCheckmarkItem_Previews: PreviewProvider {
- static let strings = AnalyticsPromptStrings()
+ static let strings = AnalyticsPromptStrings(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
static var previews: some View {
VStack(alignment: .leading) {
diff --git a/ElementX/Sources/Screens/AnalyticsSettings/AnalyticsSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/AnalyticsSettings/AnalyticsSettingsScreenCoordinator.swift
new file mode 100644
index 000000000..a27dc0021
--- /dev/null
+++ b/ElementX/Sources/Screens/AnalyticsSettings/AnalyticsSettingsScreenCoordinator.swift
@@ -0,0 +1,30 @@
+//
+// 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 Combine
+import SwiftUI
+
+final class AnalyticsSettingsScreenCoordinator: CoordinatorProtocol {
+ private let viewModel: AnalyticsSettingsScreenViewModel
+
+ init() {
+ viewModel = AnalyticsSettingsScreenViewModel()
+ }
+
+ func toPresentable() -> AnyView {
+ AnyView(AnalyticsSettingsScreen(context: viewModel.context))
+ }
+}
diff --git a/ElementX/Sources/Screens/AnalyticsSettings/AnalyticsSettingsScreenModels.swift b/ElementX/Sources/Screens/AnalyticsSettings/AnalyticsSettingsScreenModels.swift
new file mode 100644
index 000000000..915a5642a
--- /dev/null
+++ b/ElementX/Sources/Screens/AnalyticsSettings/AnalyticsSettingsScreenModels.swift
@@ -0,0 +1,46 @@
+//
+// 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 Foundation
+
+struct AnalyticsSettingsScreenViewState: BindableState {
+ /// Attributed strings created from localized HTML.
+ let strings: AnalyticsSettingsScreenStrings
+ var bindings: AnalyticsSettingsScreenViewStateBindings
+}
+
+struct AnalyticsSettingsScreenViewStateBindings {
+ var enableAnalytics: Bool
+}
+
+enum AnalyticsSettingsScreenViewAction {
+ case toggleAnalytics
+}
+
+struct AnalyticsSettingsScreenStrings {
+ let sectionFooter: AttributedString
+
+ init(termsURL: URL) {
+ let content = AttributedString(L10n.screenAnalyticsHelpUsImprove(InfoPlistReader.main.bundleDisplayName))
+ // Create the 'read terms' with a placeholder.
+ let linkPlaceholder = "{link}"
+ var readTerms = AttributedString(L10n.screenAnalyticsReadTerms(linkPlaceholder))
+ readTerms.replace(linkPlaceholder,
+ with: L10n.screenAnalyticsReadTermsContentLink,
+ asLinkTo: termsURL)
+ sectionFooter = content + "\n\n" + readTerms
+ }
+}
diff --git a/ElementX/Sources/Screens/AnalyticsSettings/AnalyticsSettingsScreenViewModel.swift b/ElementX/Sources/Screens/AnalyticsSettings/AnalyticsSettingsScreenViewModel.swift
new file mode 100644
index 000000000..d1a933ede
--- /dev/null
+++ b/ElementX/Sources/Screens/AnalyticsSettings/AnalyticsSettingsScreenViewModel.swift
@@ -0,0 +1,45 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Combine
+import SwiftUI
+
+typealias AnalyticsSettingsScreenViewModelType = StateStoreViewModel
+
+class AnalyticsSettingsScreenViewModel: AnalyticsSettingsScreenViewModelType, AnalyticsSettingsScreenViewModelProtocol {
+ init() {
+ let strings = AnalyticsSettingsScreenStrings(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
+ let bindings = AnalyticsSettingsScreenViewStateBindings(enableAnalytics: ServiceLocator.shared.settings.enableAnalytics)
+ let state = AnalyticsSettingsScreenViewState(strings: strings, bindings: bindings)
+
+ super.init(initialViewState: state)
+
+ ServiceLocator.shared.settings.$enableAnalytics
+ .weakAssign(to: \.state.bindings.enableAnalytics, on: self)
+ .store(in: &cancellables)
+ }
+
+ override func process(viewAction: AnalyticsSettingsScreenViewAction) {
+ switch viewAction {
+ case .toggleAnalytics:
+ if ServiceLocator.shared.settings.enableAnalytics {
+ ServiceLocator.shared.analytics.optOut()
+ } else {
+ ServiceLocator.shared.analytics.optIn()
+ }
+ }
+ }
+}
diff --git a/ElementX/Sources/Screens/AnalyticsSettings/AnalyticsSettingsScreenViewModelProtocol.swift b/ElementX/Sources/Screens/AnalyticsSettings/AnalyticsSettingsScreenViewModelProtocol.swift
new file mode 100644
index 000000000..e073e5424
--- /dev/null
+++ b/ElementX/Sources/Screens/AnalyticsSettings/AnalyticsSettingsScreenViewModelProtocol.swift
@@ -0,0 +1,22 @@
+//
+// 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 Combine
+
+@MainActor
+protocol AnalyticsSettingsScreenViewModelProtocol {
+ var context: AnalyticsSettingsScreenViewModelType.Context { get }
+}
diff --git a/ElementX/Sources/Screens/AnalyticsSettings/View/AnalyticsSettingsScreen.swift b/ElementX/Sources/Screens/AnalyticsSettings/View/AnalyticsSettingsScreen.swift
new file mode 100644
index 000000000..f24ce3abc
--- /dev/null
+++ b/ElementX/Sources/Screens/AnalyticsSettings/View/AnalyticsSettingsScreen.swift
@@ -0,0 +1,56 @@
+//
+// 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 SwiftUI
+
+struct AnalyticsSettingsScreen: View {
+ @ObservedObject var context: AnalyticsSettingsScreenViewModel.Context
+
+ var body: some View {
+ Form {
+ analyticsSection
+ }
+ .compoundForm()
+ .navigationTitle(L10n.commonAnalytics)
+ .navigationBarTitleDisplayMode(.inline)
+ }
+
+ var analyticsSection: some View {
+ Section {
+ Toggle(isOn: $context.enableAnalytics) {
+ Label(L10n.screenAnalyticsShareData, systemImage: "chart.bar")
+ }
+ .toggleStyle(.compoundForm)
+ .onChange(of: context.enableAnalytics) { _ in
+ context.send(viewAction: .toggleAnalytics)
+ }
+ } footer: {
+ Text(context.viewState.strings.sectionFooter)
+ .compoundFormSectionFooter()
+ .tint(.compound.textLinkExternal)
+ }
+ .compoundFormSection()
+ }
+}
+
+// MARK: - Previews
+
+struct AnalyticsSettingsScreen_Previews: PreviewProvider {
+ static var previews: some View {
+ let viewModel = AnalyticsSettingsScreenViewModel()
+ AnalyticsSettingsScreen(context: viewModel.context)
+ }
+}
diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift
index a8a727d28..67386617c 100644
--- a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift
+++ b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift
@@ -101,23 +101,30 @@ class AuthenticationCoordinator: CoordinatorProtocol {
switch action {
case .signedIn(let userSession):
- self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
+ self.userHasSignedIn(userSession: userSession)
}
}
navigationStackCoordinator.push(coordinator)
}
- private func showAnalyticsPrompt(with userSession: UserSessionProtocol) {
- let parameters = AnalyticsPromptCoordinatorParameters(userSession: userSession)
- let coordinator = AnalyticsPromptCoordinator(parameters: parameters)
-
- coordinator.callback = { [weak self] in
+ private func userHasSignedIn(userSession: UserSessionProtocol) {
+ showAnalyticsPromptIfNeeded { [weak self] in
guard let self else { return }
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
}
-
- navigationStackCoordinator.setRootCoordinator(coordinator)
+ }
+
+ private func showAnalyticsPromptIfNeeded(completion: @escaping () -> Void) {
+ guard ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt else {
+ completion()
+ return
+ }
+ let coordinator = AnalyticsPromptCoordinator()
+ coordinator.callback = {
+ completion()
+ }
+ navigationStackCoordinator.push(coordinator)
}
static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading"
diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift
index 0e5a5d083..7bab99b08 100644
--- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift
+++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift
@@ -62,11 +62,8 @@ struct HomeScreenViewState: BindableState {
let userID: String
var userDisplayName: String?
var userAvatarURL: URL?
-
var showSessionVerificationBanner = false
-
var rooms: [HomeScreenRoom] = []
-
var roomListMode: HomeScreenRoomListMode = .skeletons
/// The URL that will be shared when inviting friends to use the app.
diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift
index c969bd62d..371102a0a 100644
--- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift
+++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift
@@ -119,6 +119,7 @@ struct HomeScreen: View {
}
}
.background(Color.element.background.ignoresSafeArea())
+ .track(screen: .home)
}
@ViewBuilder
diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift
index 438fb75d8..eb7515f84 100644
--- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift
+++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift
@@ -40,6 +40,7 @@ struct RoomScreen: View {
.overlay { loadingIndicator }
.alert(item: $context.alertInfo) { $0.alert }
.sheet(item: $context.debugInfo) { TimelineItemDebugView(info: $0) }
+ .track(screen: .room)
.task(id: context.viewState.roomId) {
// Give a couple of seconds for items to load and to see them.
try? await Task.sleep(for: .seconds(2))
diff --git a/ElementX/Sources/Screens/Settings/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreenCoordinator.swift
index 9ea395b33..1f9f2aa50 100644
--- a/ElementX/Sources/Screens/Settings/SettingsScreenCoordinator.swift
+++ b/ElementX/Sources/Screens/Settings/SettingsScreenCoordinator.swift
@@ -46,8 +46,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
switch action {
case .close:
self.callback?(.dismiss)
- case .toggleAnalytics:
- self.toggleAnalytics()
+ case .analytics:
+ self.presentAnalyticsScreen()
case .reportBug:
self.presentBugReportScreen()
case .sessionVerification:
@@ -68,14 +68,11 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
// MARK: - Private
- private func toggleAnalytics() {
- if ServiceLocator.shared.settings.enableAnalytics {
- Analytics.shared.optOut()
- } else {
- Analytics.shared.optIn(with: parameters.userSession)
- }
+ private func presentAnalyticsScreen() {
+ let coordinator = AnalyticsSettingsScreenCoordinator()
+ parameters.navigationStackCoordinator?.push(coordinator)
}
-
+
private func presentBugReportScreen() {
let params = BugReportCoordinatorParameters(bugReportService: parameters.bugReportService,
userID: parameters.userSession.userID,
diff --git a/ElementX/Sources/Screens/Settings/SettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/SettingsScreenModels.swift
index 43ad6ab4e..63f95a3f1 100644
--- a/ElementX/Sources/Screens/Settings/SettingsScreenModels.swift
+++ b/ElementX/Sources/Screens/Settings/SettingsScreenModels.swift
@@ -19,7 +19,7 @@ import UIKit
enum SettingsScreenViewModelAction {
case close
- case toggleAnalytics
+ case analytics
case reportBug
case sessionVerification
case developerOptions
@@ -42,7 +42,7 @@ struct SettingsScreenViewStateBindings {
enum SettingsScreenViewAction {
case close
- case toggleAnalytics
+ case analytics
case reportBug
case sessionVerification
case logout
diff --git a/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift
index 55f0fe403..913496515 100644
--- a/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift
+++ b/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift
@@ -77,8 +77,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
switch viewAction {
case .close:
callback?(.close)
- case .toggleAnalytics:
- callback?(.toggleAnalytics)
+ case .analytics:
+ callback?(.analytics)
case .reportBug:
callback?(.reportBug)
case .logout:
diff --git a/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift
index 1139eb154..445c80c41 100644
--- a/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift
+++ b/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift
@@ -92,7 +92,7 @@ struct SettingsScreen: View {
Label(L10n.commonDeveloperOptions, systemImage: "hammer.circle")
}
.buttonStyle(.compoundForm(accessory: .navigationLink))
- .accessibilityIdentifier("sessionVerificationButton")
+ .accessibilityIdentifier("developerOptionsButton")
}
.compoundFormSection()
}
@@ -113,6 +113,14 @@ struct SettingsScreen: View {
context.send(viewAction: .changedTimelineStyle)
}
+ // Analytics
+ Button { context.send(viewAction: .analytics) } label: {
+ Label(L10n.commonAnalytics, systemImage: "chart.bar")
+ }
+ .buttonStyle(.compoundForm(accessory: .navigationLink))
+ .accessibilityIdentifier("analyticsButton")
+
+ // Report Bug
Button { context.send(viewAction: .reportBug) } label: {
Label(L10n.actionReportBug, systemImage: "questionmark.circle")
}
diff --git a/ElementX/Sources/Services/Analytics/Analytics.swift b/ElementX/Sources/Services/Analytics/Analytics.swift
index 16789b761..844dc75d7 100644
--- a/ElementX/Sources/Services/Analytics/Analytics.swift
+++ b/ElementX/Sources/Services/Analytics/Analytics.swift
@@ -31,18 +31,13 @@ import PostHog
/// into `main`, update the AnalyticsEvents Swift package in `project.yml`.
///
class Analytics {
- /// The singleton instance to be used within the Riot target.
- static let shared = Analytics()
-
/// The analytics client to send events with.
- private var client: AnalyticsClientProtocol = PostHogAnalyticsClient()
-
-// /// The monitoring client to track crashes, issues and performance
-// private var monitoringClient = SentryMonitoringClient()
-
- /// The service used to interact with account data settings.
- private var service: AnalyticsService?
-
+ private let client: AnalyticsClientProtocol
+
+ init(client: AnalyticsClientProtocol) {
+ self.client = client
+ }
+
/// Whether or not the object is enabled and sending events to the server.
var isRunning: Bool { client.isRunning }
@@ -53,13 +48,9 @@ class Analytics {
}
/// 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 userSession: UserSessionProtocol) {
+ func optIn() {
ServiceLocator.shared.settings.enableAnalytics = true
startIfEnabled()
-
- Task { await useAnalyticsSettings(from: userSession) }
}
/// Stops analytics tracking and calls `reset` to clear any IDs and event queues.
@@ -69,8 +60,7 @@ class Analytics {
// The order is important here. PostHog ignores the reset if stopped.
reset()
client.stop()
-// monitoringClient.stop()
-
+ ServiceLocator.shared.bugReportService.stop()
MXLog.info("Stopped.")
}
@@ -79,38 +69,12 @@ class Analytics {
guard ServiceLocator.shared.settings.enableAnalytics, !isRunning else { return }
client.start()
-// monitoringClient.start()
-
+ ServiceLocator.shared.bugReportService.start()
+
// Sanity check in case something went wrong.
guard client.isRunning else { return }
MXLog.info("Started.")
-
- // Catch and log crashes
-// MXLogger.logCrashes(true)
-// MXLogger.setBuildVersion(Bundle.bundleShortVersionString)
- }
-
- /// Use the analytics settings from the supplied user session to configure analytics.
- /// For now this is only used for (pseudonymous) identification.
- /// - Parameter userSession: The user session to read analytics settings from.
- func useAnalyticsSettings(from userSession: UserSessionProtocol) async {
- guard
- ServiceLocator.shared.settings.enableAnalytics,
- !ServiceLocator.shared.settings.isIdentifiedForAnalytics
- else { return }
-
- let service = AnalyticsService(userSession: userSession)
- self.service = service
-
- switch await service.settings() {
- case .success(let settings):
- identify(with: settings)
- self.service = nil
- case .failure:
- MXLog.error("Failed to use analytics settings. Will continue to run without analytics ID.")
- self.service = nil
- }
}
/// Resets the any IDs and event queues in the analytics client. This method should
@@ -119,13 +83,8 @@ class Analytics {
/// Note: **MUST** be called before stopping PostHog or the reset is ignored.
func reset() {
client.reset()
-// monitoringClient.reset()
-
+ ServiceLocator.shared.bugReportService.reset()
MXLog.info("Reset.")
- ServiceLocator.shared.settings.isIdentifiedForAnalytics = false
-
- // Stop collecting crash logs
-// MXLogger.logCrashes(false)
}
/// Flushes the event queue in the analytics client, uploading all pending events.
@@ -137,33 +96,23 @@ class Analytics {
// MARK: - Private
- /// Identify (pseudonymously) any future events with the ID from the analytics account data settings.
- /// - Parameter settings: The settings to use for identification. The ID must be set *before* calling this method.
- private func identify(with settings: AnalyticsSettings) {
- guard let id = settings.id else {
- MXLog.error("identify(with:) called before an ID has been generated.")
- return
- }
-
- client.identify(id: id)
- MXLog.info("Identified.")
- ServiceLocator.shared.settings.isIdentifiedForAnalytics = true
- }
-
/// Capture an event in the `client`.
/// - Parameter event: The event to capture.
private func capture(event: AnalyticsEventProtocol) {
+ MXLog.debug("\(event)")
client.capture(event)
}
}
// MARK: - Public tracking methods
-// The following methods are exposed for compatibility with Objective-C as
-// the `capture` method and the generated events cannot be bridged from Swift.
-extension Analytics { }
-
-// MARK: - MXAnalyticsDelegate
-
-// extension Analytics: MXAnalyticsDelegate {
-// }
+extension Analytics {
+ /// Track the presentation of a screen
+ /// - Parameter screen: The screen that was shown
+ /// - Parameter duration: An optional value representing how long the screen was shown for in milliseconds.
+ func track(screen: AnalyticsScreen, duration milliseconds: Int? = nil) {
+ MXLog.debug("\(screen)")
+ let event = AnalyticsEvent.MobileScreen(durationMs: milliseconds, screenName: screen.screenName)
+ client.screen(event)
+ }
+}
diff --git a/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift b/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift
index f601a1095..a399e372c 100644
--- a/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift
+++ b/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift
@@ -23,11 +23,7 @@ protocol AnalyticsClientProtocol {
/// Starts the analytics client reporting data.
func start()
-
- /// Associate the client with an ID. This is persisted until `reset` is called.
- /// - Parameter id: The ID to associate with the user.
- func identify(id: String)
-
+
/// Reset all stored properties and any event queues on the client. Note that
/// the client will remain active, but in a fresh unidentified state.
func reset()
@@ -54,3 +50,6 @@ protocol AnalyticsClientProtocol {
/// as part of the next event that gets captured.
func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties)
}
+
+// sourcery: AutoMockable
+extension AnalyticsClientProtocol { }
diff --git a/ElementX/Sources/Services/Analytics/AnalyticsScreen.swift b/ElementX/Sources/Services/Analytics/AnalyticsScreen.swift
new file mode 100644
index 000000000..174d4be40
--- /dev/null
+++ b/ElementX/Sources/Services/Analytics/AnalyticsScreen.swift
@@ -0,0 +1,150 @@
+//
+// 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 AnalyticsEvents
+import Foundation
+
+enum AnalyticsScreen: Int {
+ case welcome
+ case login
+ case register
+ case forgotPassword
+ case sidebar
+ case home
+ case favourites
+ case people
+ case rooms
+ case searchRooms
+ case searchMessages
+ case searchPeople
+ case searchFiles
+ case room
+ case roomPreview
+ case roomDetails
+ case roomMembers
+ case user
+ case roomSearch
+ case roomUploads
+ case roomSettings
+ case roomNotifications
+ case roomDirectory
+ case switchDirectory
+ case startChat
+ case createRoom
+ case settings
+ case settingsSecurity
+ case settingsDefaultNotifications
+ case settingsMentionsAndKeywords
+ case settingsNotifications
+ case deactivateAccount
+ case inviteFriends
+ case threadList
+ case spaceMenu
+ case spaceMembers
+ case spaceExploreRooms
+ case dialpad
+ case spaceBottomSheet
+ case invites
+ case createSpace
+
+ /// The screen name reported to the AnalyticsEvent.
+ var screenName: AnalyticsEvent.MobileScreen.ScreenName {
+ switch self {
+ case .welcome:
+ return .Welcome
+ case .login:
+ return .Login
+ case .register:
+ return .Register
+ case .forgotPassword:
+ return .ForgotPassword
+ case .sidebar:
+ return .Sidebar
+ case .home:
+ return .Home
+ case .favourites:
+ return .Favourites
+ case .people:
+ return .People
+ case .rooms:
+ return .Rooms
+ case .searchRooms:
+ return .SearchRooms
+ case .searchMessages:
+ return .SearchMessages
+ case .searchPeople:
+ return .SearchPeople
+ case .searchFiles:
+ return .SearchFiles
+ case .room:
+ return .Room
+ case .roomDetails:
+ return .RoomDetails
+ case .roomMembers:
+ return .RoomMembers
+ case .user:
+ return .User
+ case .roomPreview:
+ return .RoomPreview
+ case .roomSearch:
+ return .RoomSearch
+ case .roomUploads:
+ return .RoomUploads
+ case .roomSettings:
+ return .RoomSettings
+ case .roomNotifications:
+ return .RoomNotifications
+ case .roomDirectory:
+ return .RoomDirectory
+ case .switchDirectory:
+ return .SwitchDirectory
+ case .startChat:
+ return .StartChat
+ case .createRoom:
+ return .CreateRoom
+ case .settings:
+ return .Settings
+ case .settingsSecurity:
+ return .SettingsSecurity
+ case .settingsDefaultNotifications:
+ return .SettingsDefaultNotifications
+ case .settingsMentionsAndKeywords:
+ return .SettingsMentionsAndKeywords
+ case .settingsNotifications:
+ return .SettingsNotifications
+ case .deactivateAccount:
+ return .DeactivateAccount
+ case .inviteFriends:
+ return .InviteFriends
+ case .threadList:
+ return .ThreadList
+ case .spaceMenu:
+ return .SpaceMenu
+ case .spaceMembers:
+ return .SpaceMembers
+ case .spaceExploreRooms:
+ return .SpaceExploreRooms
+ case .dialpad:
+ return .Dialpad
+ case .spaceBottomSheet:
+ return .SpaceBottomSheet
+ case .invites:
+ return .Invites
+ case .createSpace:
+ return .CreateSpace
+ }
+ }
+}
diff --git a/ElementX/Sources/Services/Analytics/AnalyticsService.swift b/ElementX/Sources/Services/Analytics/AnalyticsService.swift
deleted file mode 100644
index ce1e9b826..000000000
--- a/ElementX/Sources/Services/Analytics/AnalyticsService.swift
+++ /dev/null
@@ -1,75 +0,0 @@
-//
-// 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 AnalyticsServiceError: Error {
- /// 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
-}
-
-/// A service responsible for handling the `im.vector.analytics` event from the user's account data.
-class AnalyticsService {
- let userSession: UserSessionProtocol
-
- /// 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
- /// contain an `id` property and if not, will add one to the account data before calling the completion.
- /// - Parameter completion: A completion handler that will be called when the request completes.
- ///
- /// The request will fail if the service's session does not have the `MXSessionStateRunning` state.
- func settings() async -> Result {
- // Only use the session if it is running otherwise we could wipe out an existing analytics ID.
- fatalWithoutUnreachableCodeWarning()
-// 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 = await userSession.clientProxy.accountDataEvent(type: AnalyticsSettings.eventType)
- switch result {
- case .failure:
- return .failure(.accountDataFailure)
- case .success(let settings):
- // The id has already be set so we are done here.
- if let settings, settings.id != nil {
- return .success(settings)
- }
-
- let newSettings = AnalyticsSettings.new(currentEvent: settings)
- switch await userSession.clientProxy.setAccountData(content: newSettings, type: AnalyticsSettings.eventType) {
- case .failure:
- MXLog.error("Failed to update analytics settings.")
- return .failure(.accountDataFailure)
- case .success:
- MXLog.debug("Successfully updated analytics settings in account data.")
- return .success(newSettings)
- }
- }
- }
-
- /// Silences a warning on some intentionally unreachable code.
- func fatalWithoutUnreachableCodeWarning() {
- fatalError("Missing running state detection.")
- }
-}
diff --git a/ElementX/Sources/Services/Analytics/AnalyticsSettings.swift b/ElementX/Sources/Services/Analytics/AnalyticsSettings.swift
deleted file mode 100644
index 96305223c..000000000
--- a/ElementX/Sources/Services/Analytics/AnalyticsSettings.swift
+++ /dev/null
@@ -1,50 +0,0 @@
-//
-// 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
-
-/// An analytics settings event from the user's account data.
-struct AnalyticsSettings: Codable {
- static let eventType = "im.vector.analytics"
-
- /// A randomly generated analytics token for this user.
- /// This is suggested to be a UUID string.
- let id: String?
-
- /// Whether the user has opted in on web or not. This is unused on iOS but necessary
- /// to store here so that it's value is preserved when updating the account data if we
- /// generated an ID on iOS.
- ///
- /// `true` if opted in on web, `false` if opted out on web and `nil` if the web prompt is not yet seen.
- private let webOptIn: Bool?
-
- enum CodingKeys: String, CodingKey {
- case id
- case webOptIn = "pseudonymousAnalyticsOptIn"
- }
-}
-
-extension AnalyticsSettings {
- /// Generates a new AnalyticsSettings value (inc an ID if necessary) based upon an
- /// existing value. This is the only way the type should be created so as to avoid wiping
- /// out the `webOptIn` value that the user may already have set.
- ///
- /// **Note:** Please don't pass a `nil` literal to this method.
- static func new(currentEvent: AnalyticsSettings?) -> AnalyticsSettings {
- AnalyticsSettings(id: currentEvent?.id ?? UUID().uuidString,
- webOptIn: currentEvent?.webOptIn)
- }
-}
diff --git a/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift b/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift
index 6a2f2381f..7f6e3cd56 100644
--- a/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift
+++ b/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift
@@ -38,16 +38,6 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
postHog?.enable()
}
- func identify(id: String) {
- if let userProperties = pendingUserProperties {
- // As user properties overwrite old ones, compactMap the dictionary to avoid resetting any missing properties
- postHog?.identify(id, properties: userProperties.properties.compactMapValues { $0 })
- pendingUserProperties = nil
- } else {
- postHog?.identify(id)
- }
- }
-
func reset() {
postHog?.reset()
pendingUserProperties = nil
@@ -65,10 +55,12 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
}
func capture(_ event: AnalyticsEventProtocol) {
+ guard isRunning else { return }
postHog?.capture(event.eventName, properties: attachUserProperties(to: event.properties))
}
func screen(_ event: AnalyticsScreenProtocol) {
+ guard isRunning else { return }
postHog?.screen(event.screenName.rawValue, properties: attachUserProperties(to: event.properties))
}
diff --git a/ElementX/Sources/Services/BugReport/BugReportService.swift b/ElementX/Sources/Services/BugReport/BugReportService.swift
index 7e08e07ef..b7b3e5aa6 100644
--- a/ElementX/Sources/Services/BugReport/BugReportService.swift
+++ b/ElementX/Sources/Services/BugReport/BugReportService.swift
@@ -39,13 +39,27 @@ class BugReportService: NSObject, BugReportServiceProtocol {
self.session = session
super.init()
- // enable SentrySDK
+ // set build version for logger
+ MXLogger.buildVersion = InfoPlistReader.main.bundleShortVersionString
+ }
+
+ // MARK: - BugReportServiceProtocol
+
+ var isRunning: Bool {
+ SentrySDK.isEnabled
+ }
+
+ var crashedLastRun: Bool {
+ SentrySDK.crashedLastRun
+ }
+
+ func start() {
+ guard !isRunning else { return }
SentrySDK.start { options in
#if DEBUG
options.enabled = false
#endif
-
- options.dsn = sentryURL.absoluteString
+ options.dsn = self.sentryURL.absoluteString
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
// We recommend adjusting this value in production.
@@ -61,19 +75,22 @@ class BugReportService: NSObject, BugReportServiceProtocol {
self?.lastCrashEventId = event.eventId.sentryIdString
}
}
-
- // also enable logging crashes, to send them with bug reports
MXLogger.logCrashes(true)
- // set build version for logger
- MXLogger.buildVersion = InfoPlistReader.main.bundleShortVersionString
+ MXLog.info("Started.")
}
-
- // MARK: - BugReportServiceProtocol
-
- var crashedLastRun: Bool {
- SentrySDK.crashedLastRun
+
+ func stop() {
+ guard isRunning else { return }
+ SentrySDK.close()
+ MXLogger.logCrashes(false)
+ MXLog.info("Stopped.")
}
-
+
+ func reset() {
+ lastCrashEventId = nil
+ MXLog.info("Reset.")
+ }
+
func crash() {
SentrySDK.crash()
}
diff --git a/ElementX/Sources/Services/BugReport/BugReportServiceProtocol.swift b/ElementX/Sources/Services/BugReport/BugReportServiceProtocol.swift
index d217523d3..445d5e09b 100644
--- a/ElementX/Sources/Services/BugReport/BugReportServiceProtocol.swift
+++ b/ElementX/Sources/Services/BugReport/BugReportServiceProtocol.swift
@@ -33,10 +33,18 @@ struct SubmitBugReportResponse: Decodable {
// sourcery: AutoMockable
protocol BugReportServiceProtocol {
+ var isRunning: Bool { get }
+
var crashedLastRun: Bool { get }
+
+ func start()
+
+ func stop()
+
+ func reset()
func crash()
-
+
func submitBugReport(_ bugReport: BugReport,
progressListener: ProgressListener?) async throws -> SubmitBugReportResponse
}
diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift
index 3ed498f80..aebec2764 100644
--- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift
+++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift
@@ -31,6 +31,8 @@ class UITestsAppCoordinator: AppCoordinatorProtocol {
AppSettings.configureWithSuiteName("io.element.elementx.uitests")
AppSettings.reset()
ServiceLocator.shared.register(appSettings: AppSettings())
+ ServiceLocator.shared.register(bugReportService: BugReportServiceMock())
+ ServiceLocator.shared.register(analytics: Analytics(client: AnalyticsClientMock()))
}
func start() {
@@ -76,8 +78,12 @@ class MockScreen: Identifiable {
userIndicatorController: MockUserIndicatorController(),
isModallyPresented: false))
case .analyticsPrompt:
- return AnalyticsPromptCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"),
- mediaProvider: MockMediaProvider())))
+ return AnalyticsPromptCoordinator()
+ case .analyticsSettingsScreen:
+ let navigationStackCoordinator = NavigationStackCoordinator()
+ let coordinator = AnalyticsSettingsScreenCoordinator()
+ navigationStackCoordinator.setRootCoordinator(coordinator)
+ return navigationStackCoordinator
case .authenticationFlow:
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift
index f44d74d72..1d8f2b8d3 100644
--- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift
+++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift
@@ -23,6 +23,7 @@ enum UITestsScreenIdentifier: String {
case authenticationFlow
case softLogout
case analyticsPrompt
+ case analyticsSettingsScreen
case simpleRegular
case simpleUpgrade
case home
diff --git a/Tools/Sourcery/sourcery_automockable_config.yml b/Tools/Sourcery/sourcery_automockable_config.yml
index 5bfc137c1..37752243e 100644
--- a/Tools/Sourcery/sourcery_automockable_config.yml
+++ b/Tools/Sourcery/sourcery_automockable_config.yml
@@ -6,4 +6,4 @@ output:
../../ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
args:
automMockableTestableImports: []
- autoMockableImports: [Combine, Foundation, MatrixRustSDK]
\ No newline at end of file
+ autoMockableImports: [Combine, Foundation, MatrixRustSDK, AnalyticsEvents]
diff --git a/UITests/Sources/AnalyticsSettingsScreenUITests.swift b/UITests/Sources/AnalyticsSettingsScreenUITests.swift
new file mode 100644
index 000000000..58b37e642
--- /dev/null
+++ b/UITests/Sources/AnalyticsSettingsScreenUITests.swift
@@ -0,0 +1,26 @@
+//
+// 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 ElementX
+import XCTest
+
+class AnalyticsSettingsScreenUITests: XCTestCase {
+ /// Verify that the analytics option screen is displayed correctly.
+ func testAnalyticsSettingsScreen() {
+ let app = Application.launch(.analyticsSettingsScreen)
+ app.assertScreenshot(.analyticsSettingsScreen)
+ }
+}
diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.analyticsPrompt.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.analyticsPrompt.png
index f82677162..14aeeb5b8 100644
--- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.analyticsPrompt.png
+++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.analyticsPrompt.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:97e77a883d7803bf18ace6204966badb5f76f382f6b8900b7aee9f8d1fcf8d7b
-size 135715
+oid sha256:aa03abfa975225c44719b6ccf91f2a36c15e36706c4a8e7dd3ab8ceba1394290
+size 122070
diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.analyticsSettingsScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.analyticsSettingsScreen.png
new file mode 100644
index 000000000..ebab2b2e8
--- /dev/null
+++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.analyticsSettingsScreen.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:788037ab6790666b292a8e985b36157ba66fbfd1acb10a2b6bbe3435ff42c474
+size 80927
diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png
index 80495b8cd..c1bccb936 100644
--- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png
+++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:13570c5df20909284b7184702f60fd49be43160ebf62dd05f4dd57560e067106
-size 102087
+oid sha256:818f6749b666dcdcc96a4fc4feeadd7cc6fc96e92ff6e1a5bd6a640e28584bc0
+size 106446
diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.analyticsPrompt.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.analyticsPrompt.png
index eda741887..1f8114234 100644
--- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.analyticsPrompt.png
+++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.analyticsPrompt.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ede0eb647b8e1f39777c7f5fc8a798f0c4399d95e22fc9bbace7baeffd1a1ce8
-size 181782
+oid sha256:84fba7b597dfa6bad58275a2cff3e8ff9e9ef4a0ba2ca6966868bc1275f44c01
+size 155925
diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.analyticsSettingsScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.analyticsSettingsScreen.png
new file mode 100644
index 000000000..0fb3e6b1c
--- /dev/null
+++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.analyticsSettingsScreen.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:736f751f1c5e635626d8465825ae0e2e6e4408b9c141e655496758ccdf2d647f
+size 91031
diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png
index 4de8d8fa8..5c51582e9 100644
--- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png
+++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:17b7a7c648de5de3b1fe9bff7cd6d77b1d5202f985b2e7377e0a2f9e1a4249ef
-size 123926
+oid sha256:1476d5003aa60c399587615e757e93bee462a5d073afcfe2ac4e28b0ac6de3db
+size 132960
diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.analyticsPrompt.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.analyticsPrompt.png
index e2b5c26b7..157d9a9ea 100644
--- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.analyticsPrompt.png
+++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.analyticsPrompt.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d0ff8a9c07666a38040d2171778a1d9604791780dcb41802a47511a02fb45e86
-size 167573
+oid sha256:27351f2174b636b50875ecccdd6fbaaf7f90841e4650653492ab05524f815c22
+size 151969
diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.analyticsSettingsScreen.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.analyticsSettingsScreen.png
new file mode 100644
index 000000000..ec3c07f6e
--- /dev/null
+++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.analyticsSettingsScreen.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:294c5e6ac8ba878099d63ea282c65f2fc5e9de3c746414b452ce19cb6ff16e49
+size 93352
diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png
index a4de72b48..d20f5aa38 100644
--- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png
+++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7eef8072fd55252bb8bafb94b9805e02d63ca117b60cfd2b613a759a1c8a0812
-size 108309
+oid sha256:868c2151283b6b357071f548fda277142783e615ca18061acc86c8f9ab4ee539
+size 112696
diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.analyticsPrompt.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.analyticsPrompt.png
index 5ad508ffb..b3a2eb9d1 100644
--- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.analyticsPrompt.png
+++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.analyticsPrompt.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4deddcc486c84e9b0bcbd92276ceb265497adb5e17aa841b471b35882ef40015
-size 195899
+oid sha256:ef70a25c245d517be3d86f3e79086f37f7d69d65658ffa1b7ce3ac82939c3710
+size 201812
diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.analyticsSettingsScreen.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.analyticsSettingsScreen.png
new file mode 100644
index 000000000..9abcd456e
--- /dev/null
+++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.analyticsSettingsScreen.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e4904c2275060e2c004f131a2ae8f77318bab86f9dd29427684813ef236d794e
+size 115503
diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png
index 40a0748bf..c5812c592 100644
--- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png
+++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:62b0c33b5f7888a7608adba56e9c85c1eda74f77104e43def618b6def39fc3fe
-size 143600
+oid sha256:98592869daf50a05bf16c6543b36642cd3434771d4e80d2d1f7e7e55f4feeac3
+size 156907
diff --git a/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift b/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift
new file mode 100644
index 000000000..b576b9cc8
--- /dev/null
+++ b/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift
@@ -0,0 +1,54 @@
+//
+// 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 XCTest
+
+@testable import ElementX
+
+@MainActor
+class AnalyticsSettingsScreenViewModelTests: XCTestCase {
+ private var applicationSettings: AppSettings!
+ private var viewModel: AnalyticsSettingsScreenViewModelProtocol!
+ private var context: AnalyticsSettingsScreenViewModelType.Context!
+
+ @MainActor override func setUpWithError() throws {
+ AppSettings.configureWithSuiteName("io.element.elementx.unitests")
+ AppSettings.reset()
+ applicationSettings = AppSettings()
+ ServiceLocator.shared.register(appSettings: applicationSettings)
+ let analyticsClient = AnalyticsClientMock()
+ analyticsClient.isRunning = false
+ ServiceLocator.shared.register(analytics: Analytics(client: analyticsClient))
+
+ viewModel = AnalyticsSettingsScreenViewModel()
+ context = viewModel.context
+ }
+
+ func testInitialState() {
+ XCTAssertFalse(context.enableAnalytics)
+ }
+
+ func testOptIn() {
+ context.send(viewAction: .toggleAnalytics)
+ XCTAssertTrue(context.enableAnalytics)
+ }
+
+ func testOptOut() {
+ applicationSettings.enableAnalytics = true
+ context.send(viewAction: .toggleAnalytics)
+ XCTAssertFalse(context.enableAnalytics)
+ }
+}
diff --git a/UnitTests/Sources/AnalyticsTests.swift b/UnitTests/Sources/AnalyticsTests.swift
index ae7ffa558..bb2f67a06 100644
--- a/UnitTests/Sources/AnalyticsTests.swift
+++ b/UnitTests/Sources/AnalyticsTests.swift
@@ -20,17 +20,26 @@ import XCTest
class AnalyticsTests: XCTestCase {
private var applicationSettings: AppSettings!
+ private var analyticsClient: AnalyticsClientMock!
+ private var bugReportService: BugReportServiceMock!
override func setUp() {
AppSettings.configureWithSuiteName("io.element.elementx.unitests")
AppSettings.reset()
applicationSettings = AppSettings()
+ ServiceLocator.shared.register(appSettings: applicationSettings)
+ bugReportService = BugReportServiceMock()
+ bugReportService.isRunning = false
+ ServiceLocator.shared.register(bugReportService: bugReportService)
+ analyticsClient = AnalyticsClientMock()
+ analyticsClient.isRunning = false
+ ServiceLocator.shared.register(analytics: Analytics(client: analyticsClient))
}
func testAnalyticsPromptNewUser() {
// Given a fresh install of the app (without PostHog analytics having been set).
// When the user is prompted for analytics.
- let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt
+ let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
// Then the prompt should be shown.
XCTAssertTrue(showPrompt, "A prompt should be shown for a new user.")
@@ -41,7 +50,7 @@ class AnalyticsTests: XCTestCase {
applicationSettings.enableAnalytics = false
// When the user is prompted for analytics
- let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt
+ let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
// Then no prompt should be shown.
XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
@@ -52,12 +61,65 @@ class AnalyticsTests: XCTestCase {
applicationSettings.enableAnalytics = true
// When the user is prompted for analytics
- let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt
+ let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
// Then no prompt should be shown.
XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
}
+ func testAnalyticsPromptNotDisplayed() {
+ // Given a fresh install of the app both Analytics and BugReportService should be disabled
+ XCTAssertFalse(ServiceLocator.shared.settings.enableAnalytics)
+ XCTAssertFalse(ServiceLocator.shared.analytics.isRunning)
+ XCTAssertFalse(analyticsClient.startCalled)
+ XCTAssertFalse(bugReportService.startCalled)
+ }
+
+ func testAnalyticsOptOut() {
+ // Given a fresh install of the app (without PostHog analytics having been set).
+ // When analytics is opt-out
+ ServiceLocator.shared.analytics.optOut()
+ // Then analytics should be disabled
+ XCTAssertFalse(applicationSettings.enableAnalytics)
+ XCTAssertFalse(ServiceLocator.shared.analytics.isRunning)
+ XCTAssertFalse(analyticsClient.isRunning)
+ XCTAssertFalse(bugReportService.isRunning)
+ // Analytics client and the bug report service should have been stopped
+ XCTAssertTrue(analyticsClient.stopCalled)
+ XCTAssertTrue(bugReportService.stopCalled)
+ }
+
+ func testAnalyticsOptIn() {
+ // Given a fresh install of the app (without PostHog analytics having been set).
+ // When analytics is opt-in
+ ServiceLocator.shared.analytics.optIn()
+ // The analytics should be enabled
+ XCTAssertTrue(applicationSettings.enableAnalytics)
+ // Analytics client and the bug report service should have been started
+ XCTAssertTrue(analyticsClient.startCalled)
+ XCTAssertTrue(bugReportService.startCalled)
+ }
+
+ func testAnalyticsStartIfNotEnabled() {
+ // Given an existing install of the app where the user previously declined the tracking
+ applicationSettings.enableAnalytics = false
+ // Analytics should not start
+ ServiceLocator.shared.analytics.startIfEnabled()
+ XCTAssertFalse(ServiceLocator.shared.settings.enableAnalytics)
+ XCTAssertFalse(analyticsClient.startCalled)
+ XCTAssertFalse(bugReportService.startCalled)
+ }
+
+ func testAnalyticsStartIfEnabled() {
+ // Given an existing install of the app where the user previously accpeted the tracking
+ applicationSettings.enableAnalytics = true
+ // Analytics should start
+ ServiceLocator.shared.analytics.startIfEnabled()
+ XCTAssertTrue(ServiceLocator.shared.settings.enableAnalytics)
+ XCTAssertTrue(analyticsClient.startCalled)
+ XCTAssertTrue(bugReportService.startCalled)
+ }
+
func testAddingUserProperties() {
// Given a client with no user properties set
let client = PostHogAnalyticsClient()
@@ -120,23 +182,4 @@ class AnalyticsTests: XCTestCase {
// Then the properties should be cleared
XCTAssertNil(client.pendingUserProperties, "The user properties should be cleared.")
}
-
- func testSendingUserPropertiesWithIdentify() {
- // Given a client with user properties set
- let client = PostHogAnalyticsClient()
- client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging,
- numFavouriteRooms: nil,
- numSpaces: nil,
- allChatsActiveFilter: nil))
- client.start()
-
- XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
- XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
-
- // When calling identify (tests run under Debug configuration so this is sent to the development instance)
- client.identify(id: UUID().uuidString)
-
- // Then the properties should be cleared
- XCTAssertNil(client.pendingUserProperties, "The user properties should be cleared.")
- }
}
diff --git a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift
index 5e0067e5e..dd74f9771 100644
--- a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift
+++ b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift
@@ -27,9 +27,14 @@ final class NotificationManagerTests: XCTestCase {
private var shouldDisplayInAppNotificationReturnValue = false
private var handleInlineReplyDelegateCalled = false
private var notificationTappedDelegateCalled = false
- private let settings = ServiceLocator.shared.settings
+ private var settings: AppSettings!
override func setUp() {
+ AppSettings.configureWithSuiteName("io.element.elementx.unitests")
+ AppSettings.reset()
+ settings = AppSettings()
+ ServiceLocator.shared.register(appSettings: settings)
+
notificationManager = NotificationManager(notificationCenter: notificationCenter)
notificationManager.start()
notificationManager.setClientProxy(clientProxy)
@@ -114,7 +119,7 @@ final class NotificationManagerTests: XCTestCase {
func test_whenStart_requestAuthorizationCalledWithCorrectParams() async throws {
notificationManager.requestAuthorization()
- await Task.yield()
+ try await Task.sleep(for: .milliseconds(100))
XCTAssertEqual(notificationCenter.requestAuthorizationOptions, [.alert, .sound, .badge])
}
diff --git a/UnitTests/Sources/SettingsViewModelTests.swift b/UnitTests/Sources/SettingsViewModelTests.swift
index c84190647..9a75f1fae 100644
--- a/UnitTests/Sources/SettingsViewModelTests.swift
+++ b/UnitTests/Sources/SettingsViewModelTests.swift
@@ -56,4 +56,15 @@ class SettingsScreenViewModelTests: XCTestCase {
await Task.yield()
XCTAssert(correctResult)
}
+
+ func testAnalytics() async throws {
+ var correctResult = false
+ viewModel.callback = { result in
+ correctResult = result == .analytics
+ }
+
+ context.send(viewAction: .analytics)
+ await Task.yield()
+ XCTAssert(correctResult)
+ }
}
diff --git a/changelog.d/106.feature b/changelog.d/106.feature
new file mode 100644
index 000000000..987fbdca3
--- /dev/null
+++ b/changelog.d/106.feature
@@ -0,0 +1 @@
+Set up Analytics to track data.
\ No newline at end of file