Set up Analytics to track data per session (#780)
This commit is contained in:
@@ -52,7 +52,8 @@ let allowList = ["stefanceriu",
|
|||||||
"aringenbach",
|
"aringenbach",
|
||||||
"flescio",
|
"flescio",
|
||||||
"Velin92",
|
"Velin92",
|
||||||
"alfogrillo"]
|
"alfogrillo",
|
||||||
|
"nimau"]
|
||||||
|
|
||||||
let requiresSignOff = !allowList.contains(where: {
|
let requiresSignOff = !allowList.contains(where: {
|
||||||
$0.caseInsensitiveCompare(danger.github.pullRequest.user.login) == .orderedSame
|
$0.caseInsensitiveCompare(danger.github.pullRequest.user.login) == .orderedSame
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
"action_view_source" = "View Source";
|
"action_view_source" = "View Source";
|
||||||
"action_yes" = "Yes";
|
"action_yes" = "Yes";
|
||||||
"common_about" = "About";
|
"common_about" = "About";
|
||||||
|
"common_analytics" = "Analytics";
|
||||||
"common_audio" = "Audio";
|
"common_audio" = "Audio";
|
||||||
"common_bubbles" = "Bubbles";
|
"common_bubbles" = "Bubbles";
|
||||||
"common_creating_room" = "Creating room…";
|
"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" = "This is the beginning of %1$@.";
|
||||||
"room_timeline_beginning_of_room_no_name" = "This is the beginning of this conversation.";
|
"room_timeline_beginning_of_room_no_name" = "This is the beginning of this conversation.";
|
||||||
"room_timeline_read_marker_title" = "New";
|
"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 <b>don't</b> 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 <b>don't</b> 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_attach_screenshot" = "Attach screenshot";
|
||||||
"screen_bug_report_contact_me" = "You may contact me if you have any follow up questions";
|
"screen_bug_report_contact_me" = "You may contact me if you have any follow up questions";
|
||||||
"screen_bug_report_edit_screenshot" = "Edit screenshot";
|
"screen_bug_report_edit_screenshot" = "Edit screenshot";
|
||||||
@@ -198,6 +210,10 @@
|
|||||||
"screen_dm_details_unblock_alert_action" = "Unblock";
|
"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_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_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_empty_list" = "No Invites";
|
||||||
"screen_invites_invited_you" = "%1$@ invited you";
|
"screen_invites_invited_you" = "%1$@ invited you";
|
||||||
"screen_login_error_deactivated_account" = "This account has been deactivated.";
|
"screen_login_error_deactivated_account" = "This account has been deactivated.";
|
||||||
|
|||||||
@@ -4,15 +4,6 @@
|
|||||||
/* Used for testing */
|
/* Used for testing */
|
||||||
"untranslated" = "Untranslated";
|
"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 <b>don\'t</b> record or profile any account data";
|
|
||||||
"analytics_opt_in_list_item_2" = "We <b>don\'t</b> share information with third parties";
|
|
||||||
"analytics_opt_in_list_item_3" = "You can turn this off anytime in settings";
|
|
||||||
|
|
||||||
// MARK: - Soft logout
|
// MARK: - Soft logout
|
||||||
|
|
||||||
"soft_logout_forgot_password" = "Forgot password";
|
"soft_logout_forgot_password" = "Forgot password";
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ class AppCoordinator: AppCoordinatorProtocol {
|
|||||||
private var userSessionFlowCoordinator: UserSessionFlowCoordinator?
|
private var userSessionFlowCoordinator: UserSessionFlowCoordinator?
|
||||||
private var authenticationCoordinator: AuthenticationCoordinator?
|
private var authenticationCoordinator: AuthenticationCoordinator?
|
||||||
|
|
||||||
private let bugReportService: BugReportServiceProtocol
|
|
||||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||||
|
|
||||||
private var userSessionCancellables = Set<AnyCancellable>()
|
private var userSessionCancellables = Set<AnyCancellable>()
|
||||||
@@ -56,11 +55,11 @@ class AppCoordinator: AppCoordinatorProtocol {
|
|||||||
|
|
||||||
Self.setupServiceLocator(navigationRootCoordinator: navigationRootCoordinator)
|
Self.setupServiceLocator(navigationRootCoordinator: navigationRootCoordinator)
|
||||||
Self.setupLogging()
|
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())
|
navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator())
|
||||||
|
|
||||||
backgroundTaskService = UIKitBackgroundTaskService {
|
backgroundTaskService = UIKitBackgroundTaskService {
|
||||||
@@ -84,7 +83,7 @@ class AppCoordinator: AppCoordinatorProtocol {
|
|||||||
wipeUserData(includingSettings: true)
|
wipeUserData(includingSettings: true)
|
||||||
}
|
}
|
||||||
ServiceLocator.shared.settings.lastVersionLaunched = currentVersion.description
|
ServiceLocator.shared.settings.lastVersionLaunched = currentVersion.description
|
||||||
|
|
||||||
setupStateMachine()
|
setupStateMachine()
|
||||||
|
|
||||||
observeApplicationState()
|
observeApplicationState()
|
||||||
@@ -114,6 +113,9 @@ class AppCoordinator: AppCoordinatorProtocol {
|
|||||||
ServiceLocator.shared.register(userIndicatorController: UserIndicatorController(rootCoordinator: navigationRootCoordinator))
|
ServiceLocator.shared.register(userIndicatorController: UserIndicatorController(rootCoordinator: navigationRootCoordinator))
|
||||||
ServiceLocator.shared.register(appSettings: AppSettings())
|
ServiceLocator.shared.register(appSettings: AppSettings())
|
||||||
ServiceLocator.shared.register(networkMonitor: NetworkMonitor())
|
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() {
|
private static func setupLogging() {
|
||||||
@@ -248,7 +250,7 @@ class AppCoordinator: AppCoordinatorProtocol {
|
|||||||
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SplashScreenCoordinator())
|
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SplashScreenCoordinator())
|
||||||
let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession,
|
let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession,
|
||||||
navigationSplitCoordinator: navigationSplitCoordinator,
|
navigationSplitCoordinator: navigationSplitCoordinator,
|
||||||
bugReportService: bugReportService,
|
bugReportService: ServiceLocator.shared.bugReportService,
|
||||||
roomTimelineControllerFactory: RoomTimelineControllerFactory())
|
roomTimelineControllerFactory: RoomTimelineControllerFactory())
|
||||||
|
|
||||||
userSessionFlowCoordinator.callback = { [weak self] action in
|
userSessionFlowCoordinator.callback = { [weak self] action in
|
||||||
@@ -292,6 +294,9 @@ class AppCoordinator: AppCoordinatorProtocol {
|
|||||||
userSessionStore.logout(userSession: userSession)
|
userSessionStore.logout(userSession: userSession)
|
||||||
tearDownUserSession()
|
tearDownUserSession()
|
||||||
|
|
||||||
|
// reset analytics
|
||||||
|
ServiceLocator.shared.analytics.reset()
|
||||||
|
|
||||||
stateMachine.processEvent(.completedSigningOut(isSoft: isSoft))
|
stateMachine.processEvent(.completedSigningOut(isSoft: isSoft))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ class AppCoordinatorStateMachine {
|
|||||||
private func configure() {
|
private func configure() {
|
||||||
stateMachine.addRoutes(event: .startWithAuthentication, transitions: [.initial => .signedOut])
|
stateMachine.addRoutes(event: .startWithAuthentication, transitions: [.initial => .signedOut])
|
||||||
stateMachine.addRoutes(event: .createdUserSession, transitions: [.signedOut => .signedIn])
|
stateMachine.addRoutes(event: .createdUserSession, transitions: [.signedOut => .signedIn])
|
||||||
|
|
||||||
stateMachine.addRoutes(event: .startWithExistingSession, transitions: [.initial => .restoringSession])
|
stateMachine.addRoutes(event: .startWithExistingSession, transitions: [.initial => .restoringSession])
|
||||||
stateMachine.addRoutes(event: .createdUserSession, transitions: [.restoringSession => .signedIn])
|
stateMachine.addRoutes(event: .createdUserSession, transitions: [.restoringSession => .signedIn])
|
||||||
stateMachine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])
|
stateMachine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ final class AppSettings: ObservableObject {
|
|||||||
case lastVersionLaunched
|
case lastVersionLaunched
|
||||||
case timelineStyle
|
case timelineStyle
|
||||||
case enableAnalytics
|
case enableAnalytics
|
||||||
case isIdentifiedForAnalytics
|
|
||||||
case enableInAppNotifications
|
case enableInAppNotifications
|
||||||
case pusherProfileTag
|
case pusherProfileTag
|
||||||
case shouldCollapseRoomStateEvents
|
case shouldCollapseRoomStateEvents
|
||||||
@@ -104,7 +103,7 @@ final class AppSettings: ObservableObject {
|
|||||||
let bugReportUISIId = "element-auto-uisi"
|
let bugReportUISIId = "element-auto-uisi"
|
||||||
|
|
||||||
// MARK: - Analytics
|
// MARK: - Analytics
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
/// The configuration to use for analytics during development. Set `isEnabled` to false to disable analytics in debug builds.
|
/// 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.
|
/// **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.
|
/// `true` when the user has opted in to send analytics.
|
||||||
@UserSetting(key: UserDefaultsKeys.enableAnalytics.rawValue, defaultValue: false, persistIn: store)
|
@UserSetting(key: UserDefaultsKeys.enableAnalytics.rawValue, defaultValue: false, persistIn: store)
|
||||||
var enableAnalytics
|
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
|
// MARK: - Room Screen
|
||||||
|
|
||||||
@UserSettingRawRepresentable(key: UserDefaultsKeys.timelineStyle.rawValue, defaultValue: TimelineStyle.bubbles, persistIn: store)
|
@UserSettingRawRepresentable(key: UserDefaultsKeys.timelineStyle.rawValue, defaultValue: TimelineStyle.bubbles, persistIn: store)
|
||||||
|
|||||||
@@ -38,4 +38,16 @@ class ServiceLocator {
|
|||||||
func register(networkMonitor: NetworkMonitor) {
|
func register(networkMonitor: NetworkMonitor) {
|
||||||
self.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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,24 +10,6 @@ import Foundation
|
|||||||
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
||||||
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||||
public enum UntranslatedL10n {
|
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 <b>don't</b> record or profile any account data
|
|
||||||
public static var analyticsOptInListItem1: String { return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_list_item_1") }
|
|
||||||
/// We <b>don't</b> 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
|
/// Camera
|
||||||
public static var mediaUploadCameraPicker: String { return UntranslatedL10n.tr("Untranslated", "media_upload_camera_picker") }
|
public static var mediaUploadCameraPicker: String { return UntranslatedL10n.tr("Untranslated", "media_upload_camera_picker") }
|
||||||
/// Document
|
/// Document
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ public enum L10n {
|
|||||||
public static var actionYes: String { return L10n.tr("Localizable", "action_yes") }
|
public static var actionYes: String { return L10n.tr("Localizable", "action_yes") }
|
||||||
/// About
|
/// About
|
||||||
public static var commonAbout: String { return L10n.tr("Localizable", "common_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
|
/// Audio
|
||||||
public static var commonAudio: String { return L10n.tr("Localizable", "common_audio") }
|
public static var commonAudio: String { return L10n.tr("Localizable", "common_audio") }
|
||||||
/// Bubbles
|
/// Bubbles
|
||||||
@@ -390,6 +392,38 @@ public enum L10n {
|
|||||||
public static func roomTimelineStateChanges(_ p1: Int) -> String {
|
public static func roomTimelineStateChanges(_ p1: Int) -> String {
|
||||||
return L10n.tr("Localizable", "room_timeline_state_changes", p1)
|
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 <b>don't</b> 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 <b>don't</b> 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
|
/// Attach screenshot
|
||||||
public static var screenBugReportAttachScreenshot: String { return L10n.tr("Localizable", "screen_bug_report_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
|
/// 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") }
|
public static var screenDmDetailsUnblockAlertDescription: String { return L10n.tr("Localizable", "screen_dm_details_unblock_alert_description") }
|
||||||
/// Unblock user
|
/// Unblock user
|
||||||
public static var screenDmDetailsUnblockUser: String { return L10n.tr("Localizable", "screen_dm_details_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
|
/// No Invites
|
||||||
public static var screenInvitesEmptyList: String { return L10n.tr("Localizable", "screen_invites_empty_list") }
|
public static var screenInvitesEmptyList: String { return L10n.tr("Localizable", "screen_invites_empty_list") }
|
||||||
/// %1$@ invited you
|
/// %1$@ invited you
|
||||||
|
|||||||
@@ -5,13 +5,159 @@
|
|||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import MatrixRustSDK
|
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 {
|
class BugReportServiceMock: BugReportServiceProtocol {
|
||||||
|
var isRunning: Bool {
|
||||||
|
get { return underlyingIsRunning }
|
||||||
|
set(value) { underlyingIsRunning = value }
|
||||||
|
}
|
||||||
|
var underlyingIsRunning: Bool!
|
||||||
var crashedLastRun: Bool {
|
var crashedLastRun: Bool {
|
||||||
get { return underlyingCrashedLastRun }
|
get { return underlyingCrashedLastRun }
|
||||||
set(value) { underlyingCrashedLastRun = value }
|
set(value) { underlyingCrashedLastRun = value }
|
||||||
}
|
}
|
||||||
var underlyingCrashedLastRun: Bool!
|
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
|
//MARK: - crash
|
||||||
|
|
||||||
var crashCallsCount = 0
|
var crashCallsCount = 0
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,21 +16,13 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AnalyticsPromptCoordinatorParameters {
|
|
||||||
/// The user session to use if analytics are enabled.
|
|
||||||
let userSession: UserSessionProtocol
|
|
||||||
}
|
|
||||||
|
|
||||||
final class AnalyticsPromptCoordinator: CoordinatorProtocol {
|
final class AnalyticsPromptCoordinator: CoordinatorProtocol {
|
||||||
private let parameters: AnalyticsPromptCoordinatorParameters
|
|
||||||
private var viewModel: AnalyticsPromptViewModel
|
private var viewModel: AnalyticsPromptViewModel
|
||||||
|
|
||||||
var callback: (@MainActor () -> Void)?
|
var callback: (@MainActor () -> Void)?
|
||||||
|
|
||||||
init(parameters: AnalyticsPromptCoordinatorParameters) {
|
init() {
|
||||||
self.parameters = parameters
|
viewModel = AnalyticsPromptViewModel()
|
||||||
|
|
||||||
viewModel = AnalyticsPromptViewModel(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
@@ -42,11 +34,11 @@ final class AnalyticsPromptCoordinator: CoordinatorProtocol {
|
|||||||
switch result {
|
switch result {
|
||||||
case .enable:
|
case .enable:
|
||||||
MXLog.info("Enable Analytics")
|
MXLog.info("Enable Analytics")
|
||||||
Analytics.shared.optIn(with: self.parameters.userSession)
|
ServiceLocator.shared.analytics.optIn()
|
||||||
self.callback?()
|
self.callback?()
|
||||||
case .disable:
|
case .disable:
|
||||||
MXLog.info("Disable Analytics")
|
MXLog.info("Disable Analytics")
|
||||||
Analytics.shared.optOut()
|
ServiceLocator.shared.analytics.optOut()
|
||||||
self.callback?()
|
self.callback?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,22 +32,24 @@ enum AnalyticsPromptViewModelAction {
|
|||||||
|
|
||||||
struct AnalyticsPromptViewState: BindableState {
|
struct AnalyticsPromptViewState: BindableState {
|
||||||
/// Attributed strings created from localized HTML.
|
/// 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
|
/// A collection of strings for the UI that need to be parsed from HTML
|
||||||
struct AnalyticsPromptStrings {
|
struct AnalyticsPromptStrings {
|
||||||
let optInContent: AttributedString
|
let optInContent: AttributedString
|
||||||
let point1 = AttributedStringBuilder().fromHTML(UntranslatedL10n.analyticsOptInListItem1) ?? AttributedString(UntranslatedL10n.analyticsOptInListItem1)
|
let point1 = AttributedStringBuilder().fromHTML(L10n.screenAnalyticsPromptDataUsage) ?? AttributedString(L10n.screenAnalyticsPromptDataUsage)
|
||||||
let point2 = AttributedStringBuilder().fromHTML(UntranslatedL10n.analyticsOptInListItem2) ?? AttributedString(UntranslatedL10n.analyticsOptInListItem2)
|
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.
|
// Create the opt in content with a placeholder.
|
||||||
let linkPlaceholder = "{link}"
|
let linkPlaceholder = "{link}"
|
||||||
var optInContent = AttributedString(UntranslatedL10n.analyticsOptInContent(InfoPlistReader.main.bundleDisplayName, linkPlaceholder))
|
var readTerms = AttributedString(L10n.screenAnalyticsPromptReadTerms(linkPlaceholder))
|
||||||
optInContent.replace(linkPlaceholder,
|
readTerms.replace(linkPlaceholder,
|
||||||
with: UntranslatedL10n.analyticsOptInContentLink,
|
with: L10n.screenAnalyticsPromptReadTermsContentLink,
|
||||||
asLinkTo: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
|
asLinkTo: termsURL)
|
||||||
self.optInContent = optInContent
|
optInContent = content + "\n\n" + readTerms
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,12 @@ import SwiftUI
|
|||||||
typealias AnalyticsPromptViewModelType = StateStoreViewModel<AnalyticsPromptViewState, AnalyticsPromptViewAction>
|
typealias AnalyticsPromptViewModelType = StateStoreViewModel<AnalyticsPromptViewState, AnalyticsPromptViewAction>
|
||||||
|
|
||||||
class AnalyticsPromptViewModel: AnalyticsPromptViewModelType, AnalyticsPromptViewModelProtocol {
|
class AnalyticsPromptViewModel: AnalyticsPromptViewModelType, AnalyticsPromptViewModelProtocol {
|
||||||
private let termsURL: URL
|
|
||||||
|
|
||||||
var callback: (@MainActor (AnalyticsPromptViewModelAction) -> Void)?
|
var callback: (@MainActor (AnalyticsPromptViewModelAction) -> Void)?
|
||||||
|
|
||||||
/// Initialize a view model with the specified prompt type and app display name.
|
/// Initialize a view model with the specified prompt type and app display name.
|
||||||
init(termsURL: URL) {
|
init() {
|
||||||
self.termsURL = termsURL
|
let promptStrings = AnalyticsPromptStrings(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
|
||||||
super.init(initialViewState: AnalyticsPromptViewState())
|
super.init(initialViewState: AnalyticsPromptViewState(strings: promptStrings))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ struct AnalyticsPrompt: View {
|
|||||||
private var mainContent: some View {
|
private var mainContent: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Image(uiImage: Asset.Images.analyticsLogo.image)
|
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)
|
.font(.element.title2Bold)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.foregroundColor(.element.primaryContent)
|
.foregroundColor(.element.primaryContent)
|
||||||
@@ -73,7 +73,7 @@ struct AnalyticsPrompt: View {
|
|||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
.background(Color.element.quinaryContent)
|
.background(Color.element.quinaryContent)
|
||||||
.padding(.vertical, 28)
|
.padding(.vertical, 20)
|
||||||
|
|
||||||
checkmarkList
|
checkmarkList
|
||||||
}
|
}
|
||||||
@@ -81,10 +81,10 @@ struct AnalyticsPrompt: View {
|
|||||||
|
|
||||||
/// The list of re-assurances about analytics.
|
/// The list of re-assurances about analytics.
|
||||||
private var checkmarkList: some View {
|
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(attributedString: context.viewState.strings.point1)
|
AnalyticsPromptCheckmarkItem(attributedString: context.viewState.strings.point2)
|
||||||
AnalyticsPromptCheckmarkItem(string: UntranslatedL10n.analyticsOptInListItem3)
|
AnalyticsPromptCheckmarkItem(string: context.viewState.strings.point3)
|
||||||
}
|
}
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.font(.element.body)
|
.font(.element.body)
|
||||||
@@ -113,7 +113,7 @@ struct AnalyticsPrompt: View {
|
|||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
struct AnalyticsPrompt_Previews: PreviewProvider {
|
struct AnalyticsPrompt_Previews: PreviewProvider {
|
||||||
static let viewModel = AnalyticsPromptViewModel(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
|
static let viewModel = AnalyticsPromptViewModel()
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
AnalyticsPrompt(context: viewModel.context)
|
AnalyticsPrompt(context: viewModel.context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ struct AnalyticsPromptCheckmarkItem: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Label { Text(attributedString) } icon: {
|
Label { Text(attributedString) } icon: {
|
||||||
Image(uiImage: Asset.Images.analyticsCheckmark.image)
|
Image(systemName: "checkmark.circle")
|
||||||
.foregroundColor(.element.accent)
|
.foregroundColor(.element.accent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ struct AnalyticsPromptCheckmarkItem: View {
|
|||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
struct AnalyticsPromptCheckmarkItem_Previews: PreviewProvider {
|
struct AnalyticsPromptCheckmarkItem_Previews: PreviewProvider {
|
||||||
static let strings = AnalyticsPromptStrings()
|
static let strings = AnalyticsPromptStrings(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<AnalyticsSettingsScreenViewState, AnalyticsSettingsScreenViewAction>
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,23 +101,30 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .signedIn(let userSession):
|
case .signedIn(let userSession):
|
||||||
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
|
self.userHasSignedIn(userSession: userSession)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navigationStackCoordinator.push(coordinator)
|
navigationStackCoordinator.push(coordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showAnalyticsPrompt(with userSession: UserSessionProtocol) {
|
private func userHasSignedIn(userSession: UserSessionProtocol) {
|
||||||
let parameters = AnalyticsPromptCoordinatorParameters(userSession: userSession)
|
showAnalyticsPromptIfNeeded { [weak self] in
|
||||||
let coordinator = AnalyticsPromptCoordinator(parameters: parameters)
|
|
||||||
|
|
||||||
coordinator.callback = { [weak self] in
|
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
|
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"
|
static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading"
|
||||||
|
|||||||
@@ -62,11 +62,8 @@ struct HomeScreenViewState: BindableState {
|
|||||||
let userID: String
|
let userID: String
|
||||||
var userDisplayName: String?
|
var userDisplayName: String?
|
||||||
var userAvatarURL: URL?
|
var userAvatarURL: URL?
|
||||||
|
|
||||||
var showSessionVerificationBanner = false
|
var showSessionVerificationBanner = false
|
||||||
|
|
||||||
var rooms: [HomeScreenRoom] = []
|
var rooms: [HomeScreenRoom] = []
|
||||||
|
|
||||||
var roomListMode: HomeScreenRoomListMode = .skeletons
|
var roomListMode: HomeScreenRoomListMode = .skeletons
|
||||||
|
|
||||||
/// The URL that will be shared when inviting friends to use the app.
|
/// The URL that will be shared when inviting friends to use the app.
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ struct HomeScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Color.element.background.ignoresSafeArea())
|
.background(Color.element.background.ignoresSafeArea())
|
||||||
|
.track(screen: .home)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ struct RoomScreen: View {
|
|||||||
.overlay { loadingIndicator }
|
.overlay { loadingIndicator }
|
||||||
.alert(item: $context.alertInfo) { $0.alert }
|
.alert(item: $context.alertInfo) { $0.alert }
|
||||||
.sheet(item: $context.debugInfo) { TimelineItemDebugView(info: $0) }
|
.sheet(item: $context.debugInfo) { TimelineItemDebugView(info: $0) }
|
||||||
|
.track(screen: .room)
|
||||||
.task(id: context.viewState.roomId) {
|
.task(id: context.viewState.roomId) {
|
||||||
// Give a couple of seconds for items to load and to see them.
|
// Give a couple of seconds for items to load and to see them.
|
||||||
try? await Task.sleep(for: .seconds(2))
|
try? await Task.sleep(for: .seconds(2))
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
|
|||||||
switch action {
|
switch action {
|
||||||
case .close:
|
case .close:
|
||||||
self.callback?(.dismiss)
|
self.callback?(.dismiss)
|
||||||
case .toggleAnalytics:
|
case .analytics:
|
||||||
self.toggleAnalytics()
|
self.presentAnalyticsScreen()
|
||||||
case .reportBug:
|
case .reportBug:
|
||||||
self.presentBugReportScreen()
|
self.presentBugReportScreen()
|
||||||
case .sessionVerification:
|
case .sessionVerification:
|
||||||
@@ -68,14 +68,11 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func toggleAnalytics() {
|
private func presentAnalyticsScreen() {
|
||||||
if ServiceLocator.shared.settings.enableAnalytics {
|
let coordinator = AnalyticsSettingsScreenCoordinator()
|
||||||
Analytics.shared.optOut()
|
parameters.navigationStackCoordinator?.push(coordinator)
|
||||||
} else {
|
|
||||||
Analytics.shared.optIn(with: parameters.userSession)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentBugReportScreen() {
|
private func presentBugReportScreen() {
|
||||||
let params = BugReportCoordinatorParameters(bugReportService: parameters.bugReportService,
|
let params = BugReportCoordinatorParameters(bugReportService: parameters.bugReportService,
|
||||||
userID: parameters.userSession.userID,
|
userID: parameters.userSession.userID,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import UIKit
|
|||||||
|
|
||||||
enum SettingsScreenViewModelAction {
|
enum SettingsScreenViewModelAction {
|
||||||
case close
|
case close
|
||||||
case toggleAnalytics
|
case analytics
|
||||||
case reportBug
|
case reportBug
|
||||||
case sessionVerification
|
case sessionVerification
|
||||||
case developerOptions
|
case developerOptions
|
||||||
@@ -42,7 +42,7 @@ struct SettingsScreenViewStateBindings {
|
|||||||
|
|
||||||
enum SettingsScreenViewAction {
|
enum SettingsScreenViewAction {
|
||||||
case close
|
case close
|
||||||
case toggleAnalytics
|
case analytics
|
||||||
case reportBug
|
case reportBug
|
||||||
case sessionVerification
|
case sessionVerification
|
||||||
case logout
|
case logout
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
|||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .close:
|
case .close:
|
||||||
callback?(.close)
|
callback?(.close)
|
||||||
case .toggleAnalytics:
|
case .analytics:
|
||||||
callback?(.toggleAnalytics)
|
callback?(.analytics)
|
||||||
case .reportBug:
|
case .reportBug:
|
||||||
callback?(.reportBug)
|
callback?(.reportBug)
|
||||||
case .logout:
|
case .logout:
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ struct SettingsScreen: View {
|
|||||||
Label(L10n.commonDeveloperOptions, systemImage: "hammer.circle")
|
Label(L10n.commonDeveloperOptions, systemImage: "hammer.circle")
|
||||||
}
|
}
|
||||||
.buttonStyle(.compoundForm(accessory: .navigationLink))
|
.buttonStyle(.compoundForm(accessory: .navigationLink))
|
||||||
.accessibilityIdentifier("sessionVerificationButton")
|
.accessibilityIdentifier("developerOptionsButton")
|
||||||
}
|
}
|
||||||
.compoundFormSection()
|
.compoundFormSection()
|
||||||
}
|
}
|
||||||
@@ -113,6 +113,14 @@ struct SettingsScreen: View {
|
|||||||
context.send(viewAction: .changedTimelineStyle)
|
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: {
|
Button { context.send(viewAction: .reportBug) } label: {
|
||||||
Label(L10n.actionReportBug, systemImage: "questionmark.circle")
|
Label(L10n.actionReportBug, systemImage: "questionmark.circle")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,18 +31,13 @@ import PostHog
|
|||||||
/// into `main`, update the AnalyticsEvents Swift package in `project.yml`.
|
/// into `main`, update the AnalyticsEvents Swift package in `project.yml`.
|
||||||
///
|
///
|
||||||
class Analytics {
|
class Analytics {
|
||||||
/// The singleton instance to be used within the Riot target.
|
|
||||||
static let shared = Analytics()
|
|
||||||
|
|
||||||
/// The analytics client to send events with.
|
/// The analytics client to send events with.
|
||||||
private var client: AnalyticsClientProtocol = PostHogAnalyticsClient()
|
private let client: AnalyticsClientProtocol
|
||||||
|
|
||||||
// /// The monitoring client to track crashes, issues and performance
|
init(client: AnalyticsClientProtocol) {
|
||||||
// private var monitoringClient = SentryMonitoringClient()
|
self.client = client
|
||||||
|
}
|
||||||
/// The service used to interact with account data settings.
|
|
||||||
private var service: AnalyticsService?
|
|
||||||
|
|
||||||
/// Whether or not the object is enabled and sending events to the server.
|
/// Whether or not the object is enabled and sending events to the server.
|
||||||
var isRunning: Bool { client.isRunning }
|
var isRunning: Bool { client.isRunning }
|
||||||
|
|
||||||
@@ -53,13 +48,9 @@ class Analytics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Opts in to analytics tracking with the supplied user session.
|
/// Opts in to analytics tracking with the supplied user session.
|
||||||
/// - Parameter userSession: The user session to use to when reading/generating the analytics ID.
|
func optIn() {
|
||||||
/// The session will be ignored if not running.
|
|
||||||
func optIn(with userSession: UserSessionProtocol) {
|
|
||||||
ServiceLocator.shared.settings.enableAnalytics = true
|
ServiceLocator.shared.settings.enableAnalytics = true
|
||||||
startIfEnabled()
|
startIfEnabled()
|
||||||
|
|
||||||
Task { await useAnalyticsSettings(from: userSession) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops analytics tracking and calls `reset` to clear any IDs and event queues.
|
/// 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.
|
// The order is important here. PostHog ignores the reset if stopped.
|
||||||
reset()
|
reset()
|
||||||
client.stop()
|
client.stop()
|
||||||
// monitoringClient.stop()
|
ServiceLocator.shared.bugReportService.stop()
|
||||||
|
|
||||||
MXLog.info("Stopped.")
|
MXLog.info("Stopped.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,38 +69,12 @@ class Analytics {
|
|||||||
guard ServiceLocator.shared.settings.enableAnalytics, !isRunning else { return }
|
guard ServiceLocator.shared.settings.enableAnalytics, !isRunning else { return }
|
||||||
|
|
||||||
client.start()
|
client.start()
|
||||||
// monitoringClient.start()
|
ServiceLocator.shared.bugReportService.start()
|
||||||
|
|
||||||
// Sanity check in case something went wrong.
|
// Sanity check in case something went wrong.
|
||||||
guard client.isRunning else { return }
|
guard client.isRunning else { return }
|
||||||
|
|
||||||
MXLog.info("Started.")
|
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
|
/// 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.
|
/// Note: **MUST** be called before stopping PostHog or the reset is ignored.
|
||||||
func reset() {
|
func reset() {
|
||||||
client.reset()
|
client.reset()
|
||||||
// monitoringClient.reset()
|
ServiceLocator.shared.bugReportService.reset()
|
||||||
|
|
||||||
MXLog.info("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.
|
/// Flushes the event queue in the analytics client, uploading all pending events.
|
||||||
@@ -137,33 +96,23 @@ class Analytics {
|
|||||||
|
|
||||||
// MARK: - Private
|
// 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`.
|
/// Capture an event in the `client`.
|
||||||
/// - Parameter event: The event to capture.
|
/// - Parameter event: The event to capture.
|
||||||
private func capture(event: AnalyticsEventProtocol) {
|
private func capture(event: AnalyticsEventProtocol) {
|
||||||
|
MXLog.debug("\(event)")
|
||||||
client.capture(event)
|
client.capture(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public tracking methods
|
// MARK: - Public tracking methods
|
||||||
|
|
||||||
// The following methods are exposed for compatibility with Objective-C as
|
extension Analytics {
|
||||||
// the `capture` method and the generated events cannot be bridged from Swift.
|
/// Track the presentation of a screen
|
||||||
extension Analytics { }
|
/// - Parameter screen: The screen that was shown
|
||||||
|
/// - Parameter duration: An optional value representing how long the screen was shown for in milliseconds.
|
||||||
// MARK: - MXAnalyticsDelegate
|
func track(screen: AnalyticsScreen, duration milliseconds: Int? = nil) {
|
||||||
|
MXLog.debug("\(screen)")
|
||||||
// extension Analytics: MXAnalyticsDelegate {
|
let event = AnalyticsEvent.MobileScreen(durationMs: milliseconds, screenName: screen.screenName)
|
||||||
// }
|
client.screen(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,11 +23,7 @@ protocol AnalyticsClientProtocol {
|
|||||||
|
|
||||||
/// Starts the analytics client reporting data.
|
/// Starts the analytics client reporting data.
|
||||||
func start()
|
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
|
/// Reset all stored properties and any event queues on the client. Note that
|
||||||
/// the client will remain active, but in a fresh unidentified state.
|
/// the client will remain active, but in a fresh unidentified state.
|
||||||
func reset()
|
func reset()
|
||||||
@@ -54,3 +50,6 @@ protocol AnalyticsClientProtocol {
|
|||||||
/// as part of the next event that gets captured.
|
/// as part of the next event that gets captured.
|
||||||
func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties)
|
func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sourcery: AutoMockable
|
||||||
|
extension AnalyticsClientProtocol { }
|
||||||
|
|||||||
150
ElementX/Sources/Services/Analytics/AnalyticsScreen.swift
Normal file
150
ElementX/Sources/Services/Analytics/AnalyticsScreen.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<AnalyticsSettings, AnalyticsServiceError> {
|
|
||||||
// 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<AnalyticsSettings?, ClientProxyError> = 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.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,16 +38,6 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
|
|||||||
postHog?.enable()
|
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() {
|
func reset() {
|
||||||
postHog?.reset()
|
postHog?.reset()
|
||||||
pendingUserProperties = nil
|
pendingUserProperties = nil
|
||||||
@@ -65,10 +55,12 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func capture(_ event: AnalyticsEventProtocol) {
|
func capture(_ event: AnalyticsEventProtocol) {
|
||||||
|
guard isRunning else { return }
|
||||||
postHog?.capture(event.eventName, properties: attachUserProperties(to: event.properties))
|
postHog?.capture(event.eventName, properties: attachUserProperties(to: event.properties))
|
||||||
}
|
}
|
||||||
|
|
||||||
func screen(_ event: AnalyticsScreenProtocol) {
|
func screen(_ event: AnalyticsScreenProtocol) {
|
||||||
|
guard isRunning else { return }
|
||||||
postHog?.screen(event.screenName.rawValue, properties: attachUserProperties(to: event.properties))
|
postHog?.screen(event.screenName.rawValue, properties: attachUserProperties(to: event.properties))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,13 +39,27 @@ class BugReportService: NSObject, BugReportServiceProtocol {
|
|||||||
self.session = session
|
self.session = session
|
||||||
super.init()
|
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
|
SentrySDK.start { options in
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
options.enabled = false
|
options.enabled = false
|
||||||
#endif
|
#endif
|
||||||
|
options.dsn = self.sentryURL.absoluteString
|
||||||
options.dsn = sentryURL.absoluteString
|
|
||||||
|
|
||||||
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
|
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
|
||||||
// We recommend adjusting this value in production.
|
// We recommend adjusting this value in production.
|
||||||
@@ -61,19 +75,22 @@ class BugReportService: NSObject, BugReportServiceProtocol {
|
|||||||
self?.lastCrashEventId = event.eventId.sentryIdString
|
self?.lastCrashEventId = event.eventId.sentryIdString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// also enable logging crashes, to send them with bug reports
|
|
||||||
MXLogger.logCrashes(true)
|
MXLogger.logCrashes(true)
|
||||||
// set build version for logger
|
MXLog.info("Started.")
|
||||||
MXLogger.buildVersion = InfoPlistReader.main.bundleShortVersionString
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - BugReportServiceProtocol
|
func stop() {
|
||||||
|
guard isRunning else { return }
|
||||||
var crashedLastRun: Bool {
|
SentrySDK.close()
|
||||||
SentrySDK.crashedLastRun
|
MXLogger.logCrashes(false)
|
||||||
|
MXLog.info("Stopped.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reset() {
|
||||||
|
lastCrashEventId = nil
|
||||||
|
MXLog.info("Reset.")
|
||||||
|
}
|
||||||
|
|
||||||
func crash() {
|
func crash() {
|
||||||
SentrySDK.crash()
|
SentrySDK.crash()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,18 @@ struct SubmitBugReportResponse: Decodable {
|
|||||||
|
|
||||||
// sourcery: AutoMockable
|
// sourcery: AutoMockable
|
||||||
protocol BugReportServiceProtocol {
|
protocol BugReportServiceProtocol {
|
||||||
|
var isRunning: Bool { get }
|
||||||
|
|
||||||
var crashedLastRun: Bool { get }
|
var crashedLastRun: Bool { get }
|
||||||
|
|
||||||
|
func start()
|
||||||
|
|
||||||
|
func stop()
|
||||||
|
|
||||||
|
func reset()
|
||||||
|
|
||||||
func crash()
|
func crash()
|
||||||
|
|
||||||
func submitBugReport(_ bugReport: BugReport,
|
func submitBugReport(_ bugReport: BugReport,
|
||||||
progressListener: ProgressListener?) async throws -> SubmitBugReportResponse
|
progressListener: ProgressListener?) async throws -> SubmitBugReportResponse
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ class UITestsAppCoordinator: AppCoordinatorProtocol {
|
|||||||
AppSettings.configureWithSuiteName("io.element.elementx.uitests")
|
AppSettings.configureWithSuiteName("io.element.elementx.uitests")
|
||||||
AppSettings.reset()
|
AppSettings.reset()
|
||||||
ServiceLocator.shared.register(appSettings: AppSettings())
|
ServiceLocator.shared.register(appSettings: AppSettings())
|
||||||
|
ServiceLocator.shared.register(bugReportService: BugReportServiceMock())
|
||||||
|
ServiceLocator.shared.register(analytics: Analytics(client: AnalyticsClientMock()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
@@ -76,8 +78,12 @@ class MockScreen: Identifiable {
|
|||||||
userIndicatorController: MockUserIndicatorController(),
|
userIndicatorController: MockUserIndicatorController(),
|
||||||
isModallyPresented: false))
|
isModallyPresented: false))
|
||||||
case .analyticsPrompt:
|
case .analyticsPrompt:
|
||||||
return AnalyticsPromptCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"),
|
return AnalyticsPromptCoordinator()
|
||||||
mediaProvider: MockMediaProvider())))
|
case .analyticsSettingsScreen:
|
||||||
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
|
let coordinator = AnalyticsSettingsScreenCoordinator()
|
||||||
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
|
return navigationStackCoordinator
|
||||||
case .authenticationFlow:
|
case .authenticationFlow:
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
let coordinator = AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
|
let coordinator = AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ enum UITestsScreenIdentifier: String {
|
|||||||
case authenticationFlow
|
case authenticationFlow
|
||||||
case softLogout
|
case softLogout
|
||||||
case analyticsPrompt
|
case analyticsPrompt
|
||||||
|
case analyticsSettingsScreen
|
||||||
case simpleRegular
|
case simpleRegular
|
||||||
case simpleUpgrade
|
case simpleUpgrade
|
||||||
case home
|
case home
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ output:
|
|||||||
../../ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
|
../../ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
|
||||||
args:
|
args:
|
||||||
automMockableTestableImports: []
|
automMockableTestableImports: []
|
||||||
autoMockableImports: [Combine, Foundation, MatrixRustSDK]
|
autoMockableImports: [Combine, Foundation, MatrixRustSDK, AnalyticsEvents]
|
||||||
|
|||||||
26
UITests/Sources/AnalyticsSettingsScreenUITests.swift
Normal file
26
UITests/Sources/AnalyticsSettingsScreenUITests.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:97e77a883d7803bf18ace6204966badb5f76f382f6b8900b7aee9f8d1fcf8d7b
|
oid sha256:aa03abfa975225c44719b6ccf91f2a36c15e36706c4a8e7dd3ab8ceba1394290
|
||||||
size 135715
|
size 122070
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:788037ab6790666b292a8e985b36157ba66fbfd1acb10a2b6bbe3435ff42c474
|
||||||
|
size 80927
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:13570c5df20909284b7184702f60fd49be43160ebf62dd05f4dd57560e067106
|
oid sha256:818f6749b666dcdcc96a4fc4feeadd7cc6fc96e92ff6e1a5bd6a640e28584bc0
|
||||||
size 102087
|
size 106446
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ede0eb647b8e1f39777c7f5fc8a798f0c4399d95e22fc9bbace7baeffd1a1ce8
|
oid sha256:84fba7b597dfa6bad58275a2cff3e8ff9e9ef4a0ba2ca6966868bc1275f44c01
|
||||||
size 181782
|
size 155925
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:736f751f1c5e635626d8465825ae0e2e6e4408b9c141e655496758ccdf2d647f
|
||||||
|
size 91031
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:17b7a7c648de5de3b1fe9bff7cd6d77b1d5202f985b2e7377e0a2f9e1a4249ef
|
oid sha256:1476d5003aa60c399587615e757e93bee462a5d073afcfe2ac4e28b0ac6de3db
|
||||||
size 123926
|
size 132960
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d0ff8a9c07666a38040d2171778a1d9604791780dcb41802a47511a02fb45e86
|
oid sha256:27351f2174b636b50875ecccdd6fbaaf7f90841e4650653492ab05524f815c22
|
||||||
size 167573
|
size 151969
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:294c5e6ac8ba878099d63ea282c65f2fc5e9de3c746414b452ce19cb6ff16e49
|
||||||
|
size 93352
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7eef8072fd55252bb8bafb94b9805e02d63ca117b60cfd2b613a759a1c8a0812
|
oid sha256:868c2151283b6b357071f548fda277142783e615ca18061acc86c8f9ab4ee539
|
||||||
size 108309
|
size 112696
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4deddcc486c84e9b0bcbd92276ceb265497adb5e17aa841b471b35882ef40015
|
oid sha256:ef70a25c245d517be3d86f3e79086f37f7d69d65658ffa1b7ce3ac82939c3710
|
||||||
size 195899
|
size 201812
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e4904c2275060e2c004f131a2ae8f77318bab86f9dd29427684813ef236d794e
|
||||||
|
size 115503
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:62b0c33b5f7888a7608adba56e9c85c1eda74f77104e43def618b6def39fc3fe
|
oid sha256:98592869daf50a05bf16c6543b36642cd3434771d4e80d2d1f7e7e55f4feeac3
|
||||||
size 143600
|
size 156907
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,17 +20,26 @@ import XCTest
|
|||||||
|
|
||||||
class AnalyticsTests: XCTestCase {
|
class AnalyticsTests: XCTestCase {
|
||||||
private var applicationSettings: AppSettings!
|
private var applicationSettings: AppSettings!
|
||||||
|
private var analyticsClient: AnalyticsClientMock!
|
||||||
|
private var bugReportService: BugReportServiceMock!
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
AppSettings.configureWithSuiteName("io.element.elementx.unitests")
|
AppSettings.configureWithSuiteName("io.element.elementx.unitests")
|
||||||
AppSettings.reset()
|
AppSettings.reset()
|
||||||
applicationSettings = AppSettings()
|
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() {
|
func testAnalyticsPromptNewUser() {
|
||||||
// Given a fresh install of the app (without PostHog analytics having been set).
|
// Given a fresh install of the app (without PostHog analytics having been set).
|
||||||
// When the user is prompted for analytics.
|
// When the user is prompted for analytics.
|
||||||
let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt
|
let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
|
||||||
|
|
||||||
// Then the prompt should be shown.
|
// Then the prompt should be shown.
|
||||||
XCTAssertTrue(showPrompt, "A prompt should be shown for a new user.")
|
XCTAssertTrue(showPrompt, "A prompt should be shown for a new user.")
|
||||||
@@ -41,7 +50,7 @@ class AnalyticsTests: XCTestCase {
|
|||||||
applicationSettings.enableAnalytics = false
|
applicationSettings.enableAnalytics = false
|
||||||
|
|
||||||
// When the user is prompted for analytics
|
// When the user is prompted for analytics
|
||||||
let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt
|
let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
|
||||||
|
|
||||||
// Then no prompt should be shown.
|
// Then no prompt should be shown.
|
||||||
XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
|
XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
|
||||||
@@ -52,12 +61,65 @@ class AnalyticsTests: XCTestCase {
|
|||||||
applicationSettings.enableAnalytics = true
|
applicationSettings.enableAnalytics = true
|
||||||
|
|
||||||
// When the user is prompted for analytics
|
// When the user is prompted for analytics
|
||||||
let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt
|
let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
|
||||||
|
|
||||||
// Then no prompt should be shown.
|
// Then no prompt should be shown.
|
||||||
XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
|
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() {
|
func testAddingUserProperties() {
|
||||||
// Given a client with no user properties set
|
// Given a client with no user properties set
|
||||||
let client = PostHogAnalyticsClient()
|
let client = PostHogAnalyticsClient()
|
||||||
@@ -120,23 +182,4 @@ class AnalyticsTests: XCTestCase {
|
|||||||
// Then the properties should be cleared
|
// Then the properties should be cleared
|
||||||
XCTAssertNil(client.pendingUserProperties, "The user 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.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,14 @@ final class NotificationManagerTests: XCTestCase {
|
|||||||
private var shouldDisplayInAppNotificationReturnValue = false
|
private var shouldDisplayInAppNotificationReturnValue = false
|
||||||
private var handleInlineReplyDelegateCalled = false
|
private var handleInlineReplyDelegateCalled = false
|
||||||
private var notificationTappedDelegateCalled = false
|
private var notificationTappedDelegateCalled = false
|
||||||
private let settings = ServiceLocator.shared.settings
|
private var settings: AppSettings!
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
|
AppSettings.configureWithSuiteName("io.element.elementx.unitests")
|
||||||
|
AppSettings.reset()
|
||||||
|
settings = AppSettings()
|
||||||
|
ServiceLocator.shared.register(appSettings: settings)
|
||||||
|
|
||||||
notificationManager = NotificationManager(notificationCenter: notificationCenter)
|
notificationManager = NotificationManager(notificationCenter: notificationCenter)
|
||||||
notificationManager.start()
|
notificationManager.start()
|
||||||
notificationManager.setClientProxy(clientProxy)
|
notificationManager.setClientProxy(clientProxy)
|
||||||
@@ -114,7 +119,7 @@ final class NotificationManagerTests: XCTestCase {
|
|||||||
|
|
||||||
func test_whenStart_requestAuthorizationCalledWithCorrectParams() async throws {
|
func test_whenStart_requestAuthorizationCalledWithCorrectParams() async throws {
|
||||||
notificationManager.requestAuthorization()
|
notificationManager.requestAuthorization()
|
||||||
await Task.yield()
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
XCTAssertEqual(notificationCenter.requestAuthorizationOptions, [.alert, .sound, .badge])
|
XCTAssertEqual(notificationCenter.requestAuthorizationOptions, [.alert, .sound, .badge])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,4 +56,15 @@ class SettingsScreenViewModelTests: XCTestCase {
|
|||||||
await Task.yield()
|
await Task.yield()
|
||||||
XCTAssert(correctResult)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
changelog.d/106.feature
Normal file
1
changelog.d/106.feature
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Set up Analytics to track data.
|
||||||
Reference in New Issue
Block a user