diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index ed8cd8757..565ee3fc2 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -431,6 +431,7 @@ 6860721DB3091BE08164C132 /* MapAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B48B7AD4908C5C374517B892 /* MapAssets.xcassets */; }; 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; 695825D20A761C678809345D /* MessageForwardingScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52135BD9E0E7A091688F627A /* MessageForwardingScreenModels.swift */; }; + 69A9B430397C15075D86193F /* UserPropertiesExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66AFD800AF033D8B0D11191A /* UserPropertiesExt.swift */; }; 69B3C6010B42010F591FC3CB /* RoomRolesAndPermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AF829F12FDC99717082D9 /* RoomRolesAndPermissionsScreenViewModel.swift */; }; 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; 69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */; }; @@ -1533,6 +1534,7 @@ 6663BFB9FDB8752562CD12CA /* AuthenticationStartScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenCoordinator.swift; sourceTree = ""; }; 667DD3A9D932D7D9EB380CAA /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sk; path = sk.lproj/Localizable.stringsdict; sourceTree = ""; }; 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadingPreprocessor.swift; sourceTree = ""; }; + 66AFD800AF033D8B0D11191A /* UserPropertiesExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPropertiesExt.swift; sourceTree = ""; }; 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = ""; }; 6722709BD6178E10B70C9641 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/SAS.strings; sourceTree = ""; }; 68010886142843705E342645 /* ProgressMaskModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressMaskModifier.swift; sourceTree = ""; }; @@ -3120,6 +3122,7 @@ 5CEEAE1BFAACD6C96B6DB731 /* PHGPostHogProtocol.swift */, 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */, 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */, + 66AFD800AF033D8B0D11191A /* UserPropertiesExt.swift */, 3A304097A59704AC9B869EC6 /* Helpers */, ); path = Analytics; @@ -6555,6 +6558,7 @@ D4CB979EB4FE26AAD9F9A72B /* UserProfileScreenModels.swift in Sources */, AC1DB27A4134470846BE49F6 /* UserProfileScreenViewModel.swift in Sources */, 46A183C6125A669AEB005699 /* UserProfileScreenViewModelProtocol.swift in Sources */, + 69A9B430397C15075D86193F /* UserPropertiesExt.swift in Sources */, 8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */, 4A618590DEB72C4F186BFED4 /* UserSessionFlowCoordinator.swift in Sources */, 3113065AABBC14CEAE6843FA /* UserSessionFlowCoordinatorStateMachine.swift in Sources */, @@ -7378,7 +7382,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-analytics-events"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.20.0; + minimumVersion = 0.21.0; }; }; C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b16fd01f1..7cda37262 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -121,8 +121,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-analytics-events", "state" : { - "revision" : "e4e49896331c9dcf7346c90529a9aad7944a3259", - "version" : "0.20.0" + "revision" : "f10d044b9eaf35871bbb61cdfdb8f93a04e429e0", + "version" : "0.21.0" } }, { diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 17e2b464d..7d88770fa 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -203,6 +203,45 @@ class AnalyticsClientMock: AnalyticsClientProtocol { screenReceivedInvocations.append(event) screenClosure?(event) } + //MARK: - updateUserProperties + + var updateUserPropertiesUnderlyingCallsCount = 0 + var updateUserPropertiesCallsCount: Int { + get { + if Thread.isMainThread { + return updateUserPropertiesUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = updateUserPropertiesUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + updateUserPropertiesUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + updateUserPropertiesUnderlyingCallsCount = newValue + } + } + } + } + var updateUserPropertiesCalled: Bool { + return updateUserPropertiesCallsCount > 0 + } + var updateUserPropertiesReceivedEvent: AnalyticsEvent.UserProperties? + var updateUserPropertiesReceivedInvocations: [AnalyticsEvent.UserProperties] = [] + var updateUserPropertiesClosure: ((AnalyticsEvent.UserProperties) -> Void)? + + func updateUserProperties(_ event: AnalyticsEvent.UserProperties) { + updateUserPropertiesCallsCount += 1 + updateUserPropertiesReceivedEvent = event + updateUserPropertiesReceivedInvocations.append(event) + updateUserPropertiesClosure?(event) + } } class AppLockServiceMock: AppLockServiceProtocol { var isMandatory: Bool { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index d23e1fa16..5d1bffbdf 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import AnalyticsEvents import Combine import SwiftUI @@ -86,6 +87,21 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol } .store(in: &cancellables) + userSession.sessionSecurityStatePublisher + .receive(on: DispatchQueue.main) + .filter { state in + state.verificationState != .unknown + && state.recoveryState != .settingUp + && state.recoveryState != .unknown + } + .sink { [weak self] state in + guard let self else { return } + + self.analyticsService.updateUserProperties(AnalyticsEvent.newVerificationStateUserProperty(verificationState: state.verificationState, recoveryState: state.recoveryState)) + self.analyticsService.trackSessionSecurityState(state) + } + .store(in: &cancellables) + selectedRoomPublisher .weakAssign(to: \.state.selectedRoomID, on: self) .store(in: &cancellables) diff --git a/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift b/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift index c7efd4e25..465941667 100644 --- a/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift +++ b/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift @@ -38,6 +38,10 @@ protocol AnalyticsClientProtocol { /// Capture the supplied analytics screen event. /// - Parameter event: The screen event to capture. func screen(_ event: AnalyticsScreenProtocol) + + /// Updates the user properties + /// - Parameter userProperties: The properties event to capture. + func updateUserProperties(_ event: AnalyticsEvent.UserProperties) } // sourcery: AutoMockable diff --git a/ElementX/Sources/Services/Analytics/AnalyticsService.swift b/ElementX/Sources/Services/Analytics/AnalyticsService.swift index e32f9a52b..0e363abef 100644 --- a/ElementX/Sources/Services/Analytics/AnalyticsService.swift +++ b/ElementX/Sources/Services/Analytics/AnalyticsService.swift @@ -223,4 +223,39 @@ extension AnalyticsService { let role = role.map(AnalyticsEvent.RoomModeration.Role.init) capture(event: AnalyticsEvent.RoomModeration(action: action, role: role)) } + + func trackSessionSecurityState(_ state: SessionSecurityState) { + let analyticsVerificationState: AnalyticsEvent.CryptoSessionStateChange.VerificationState + + switch state.verificationState { + case .unknown: + return + case .verified: + analyticsVerificationState = .Verified + case .unverified: + analyticsVerificationState = .NotVerified + } + + let analyticsRecoveryState: AnalyticsEvent.CryptoSessionStateChange.RecoveryState + + switch state.recoveryState { + case .enabled: + analyticsRecoveryState = .Enabled + case .disabled: + analyticsRecoveryState = .Disabled + case .incomplete: + analyticsRecoveryState = .Incomplete + case .unknown: + return + case .settingUp: + return + } + + let event = AnalyticsEvent.CryptoSessionStateChange(recoveryState: analyticsRecoveryState, verificationState: analyticsVerificationState) + client.capture(event) + } + + func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties) { + client.updateUserProperties(userProperties) + } } diff --git a/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift b/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift index be7f9083f..61b81a5c9 100644 --- a/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift +++ b/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift @@ -88,7 +88,9 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { self.pendingUserProperties = AnalyticsEvent.UserProperties(allChatsActiveFilter: userProperties.allChatsActiveFilter ?? pendingUserProperties.allChatsActiveFilter, ftueUseCaseSelection: userProperties.ftueUseCaseSelection ?? pendingUserProperties.ftueUseCaseSelection, numFavouriteRooms: userProperties.numFavouriteRooms ?? pendingUserProperties.numFavouriteRooms, - numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces) + numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces, + recoveryState: userProperties.recoveryState ?? pendingUserProperties.recoveryState, + verificationState: userProperties.verificationState ?? pendingUserProperties.verificationState) } func updateSuperProperties(_ updatedProperties: AnalyticsEvent.SuperProperties) { diff --git a/ElementX/Sources/Services/Analytics/UserPropertiesExt.swift b/ElementX/Sources/Services/Analytics/UserPropertiesExt.swift new file mode 100644 index 000000000..6228e9450 --- /dev/null +++ b/ElementX/Sources/Services/Analytics/UserPropertiesExt.swift @@ -0,0 +1,46 @@ +// +// Copyright 2024 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 + +extension AnalyticsEvent { + static func newVerificationStateUserProperty(verificationState: SessionVerificationState, recoveryState: SecureBackupRecoveryState) -> UserProperties { + let analyticsVerificationState: AnalyticsEvent.UserProperties.VerificationState? = switch verificationState { + case .unknown: + nil + case .verified: + .Verified + case .unverified: + .NotVerified + } + + let analyticsRecoveryState: AnalyticsEvent.UserProperties.RecoveryState? = switch recoveryState { + case .enabled: + .Enabled + case .disabled: + .Disabled + case .incomplete: + .Incomplete + case .unknown: + nil + case .settingUp: + nil + } + + return UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: nil, numFavouriteRooms: nil, numSpaces: nil, recoveryState: analyticsRecoveryState, verificationState: analyticsVerificationState) + } +} diff --git a/UnitTests/Sources/AnalyticsTests.swift b/UnitTests/Sources/AnalyticsTests.swift index 1298df195..9dcb69eb1 100644 --- a/UnitTests/Sources/AnalyticsTests.swift +++ b/UnitTests/Sources/AnalyticsTests.swift @@ -142,13 +142,15 @@ class AnalyticsTests: XCTestCase { client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: 4, - numSpaces: 5)) + numSpaces: 5, recoveryState: .Disabled, verificationState: .Verified)) // Then the properties should be cached XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.") XCTAssertEqual(client.pendingUserProperties?.numFavouriteRooms, 4, "The number of favorite rooms should match.") XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should match.") + XCTAssertEqual(client.pendingUserProperties?.verificationState, AnalyticsEvent.UserProperties.VerificationState.Verified, "The verification state should match.") + XCTAssertEqual(client.pendingUserProperties?.recoveryState, AnalyticsEvent.UserProperties.RecoveryState.Disabled, "The recovery state should match.") } func testMergingUserProperties() { @@ -156,7 +158,7 @@ class AnalyticsTests: XCTestCase { let client = PostHogAnalyticsClient() client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, - numSpaces: nil)) + numSpaces: nil, recoveryState: nil, verificationState: nil)) XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.") @@ -166,7 +168,7 @@ class AnalyticsTests: XCTestCase { // When updating the number of spaced client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: nil, numFavouriteRooms: 4, - numSpaces: 5)) + numSpaces: 5, recoveryState: nil, verificationState: nil)) // Then the new properties should be updated and the existing properties should remain unchanged XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") @@ -183,7 +185,7 @@ class AnalyticsTests: XCTestCase { client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, - numSpaces: nil)) + numSpaces: nil, recoveryState: nil, verificationState: nil)) XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.") diff --git a/project.yml b/project.yml index 6e02a29f8..b072986d2 100644 --- a/project.yml +++ b/project.yml @@ -57,7 +57,7 @@ packages: # path: ../compound-ios AnalyticsEvents: url: https://github.com/matrix-org/matrix-analytics-events - minorVersion: 0.20.0 + minorVersion: 0.21.0 # path: ../matrix-analytics-events Emojibase: url: https://github.com/matrix-org/emojibase-bindings