Allow the services that are configured by secrets to be disabled. (#3961)

* Make the map tiler key optional.

* Make the bug report URL optional.

* Make the sentry URL optional.

* Make the analytics configuration optional and handle consent taking Sentry into account.

* Stop prompting users to report crashes when Sentry is disabled.
This commit is contained in:
Doug
2025-04-01 09:24:12 +01:00
committed by GitHub
parent 36903bf3fb
commit 5e50823223
71 changed files with 321 additions and 134 deletions
+4
View File
@@ -786,6 +786,7 @@
9A0326D2375075871D2AB537 /* ResolveVerifiedUserSendFailureScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 574CB70E82D7EAEA538E4135 /* ResolveVerifiedUserSendFailureScreenViewModel.swift */; };
9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; };
9A4E3D5AA44B041DAC3A0D81 /* OIDCAuthenticationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92390F9FA98255440A6BF5F8 /* OIDCAuthenticationPresenter.swift */; };
9A8E6FCD86B89970EC72EFD8 /* BugReportServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F7FC9580CABF797A2E6213A /* BugReportServiceMock.swift */; };
9AC5F8142413862A9E3A2D98 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */; };
9B03943616A1147539DF7F08 /* RoomChangePermissionsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41D041A857614A9AE13C7795 /* RoomChangePermissionsScreenViewModelTests.swift */; };
9B356742E035D90A8BB5CABE /* ProposedViewSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */; };
@@ -2029,6 +2030,7 @@
8F062DD2CCD95DC33528A16F /* KnockRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestProxy.swift; sourceTree = "<group>"; };
8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = "<group>"; };
8F6210134203BE1F2DD5C679 /* RoomDirectoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectoryCell.swift; sourceTree = "<group>"; };
8F7FC9580CABF797A2E6213A /* BugReportServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceMock.swift; sourceTree = "<group>"; };
8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenViewModelTests.swift; sourceTree = "<group>"; };
8FB89DC7F9A4A91020037001 /* AuthenticationStartScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenViewModelTests.swift; sourceTree = "<group>"; };
8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
@@ -3215,6 +3217,7 @@
0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */,
4760CE2128FBC217304272AB /* AuthenticationClientBuilderMock.swift */,
9FD7E851E2BA8C5A8D284B2A /* BannedRoomProxyMock.swift */,
8F7FC9580CABF797A2E6213A /* BugReportServiceMock.swift */,
E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */,
4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */,
E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */,
@@ -6947,6 +6950,7 @@
8D71E5E53F372202379BECCE /* BugReportScreenViewModel.swift in Sources */,
B4A0C69370E6008A971463E7 /* BugReportScreenViewModelProtocol.swift in Sources */,
3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */,
9A8E6FCD86B89970EC72EFD8 /* BugReportServiceMock.swift in Sources */,
172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */,
E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */,
6BAD956B909A6E29F6CC6E7C /* ButtonStyle.swift in Sources */,
@@ -355,8 +355,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
private static func setupServiceLocator(appSettings: AppSettings, appHooks: AppHooks) {
ServiceLocator.shared.register(userIndicatorController: UserIndicatorController())
ServiceLocator.shared.register(appSettings: appSettings)
ServiceLocator.shared.register(bugReportService: BugReportService(withBaseURL: appSettings.bugReportServiceBaseURL,
applicationId: appSettings.bugReportApplicationId,
ServiceLocator.shared.register(bugReportService: BugReportService(baseURL: appSettings.bugReportServiceBaseURL,
applicationID: appSettings.bugReportApplicationID,
sdkGitSHA: sdkGitSha(),
maxUploadSize: appSettings.bugReportMaxUploadSize,
appHooks: appHooks))
@@ -782,6 +782,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
}
private static func setupSentry(appSettings: AppSettings) {
guard let bugReportSentryURL = appSettings.bugReportSentryURL else { return }
let options: Options = .init()
#if DEBUG
@@ -790,7 +792,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
options.enabled = appSettings.analyticsConsentState == .optedIn
#endif
options.dsn = appSettings.bugReportSentryURL.absoluteString
options.dsn = bugReportSentryURL.absoluteString
if AppSettings.isDevelopmentBuild {
options.environment = "development"
+10 -12
View File
@@ -223,23 +223,21 @@ final class AppSettings {
// MARK: - Bug report
let bugReportServiceBaseURL: URL! = URL(string: Secrets.rageshakeServerURL)
let bugReportSentryURL: URL! = URL(string: Secrets.sentryDSN)
// Use the name allocated by the bug report server
let bugReportApplicationId = "element-x-ios"
let bugReportServiceBaseURL: URL? = URL(string: Secrets.rageshakeServerURL)! // swiftlint:disable:this force_unwrapping
let bugReportSentryURL: URL? = URL(string: Secrets.sentryDSN)! // swiftlint:disable:this force_unwrapping
/// The name allocated by the bug report server
let bugReportApplicationID = "element-x-ios"
/// The maximum size of the upload request. Default value is just below CloudFlare's max request size.
let bugReportMaxUploadSize = 50 * 1024 * 1024
// MARK: - Analytics
/// The configuration to use for analytics. Set `isEnabled` to false to disable analytics.
///
/// **Note:** Analytics are disabled by default for forks. If you are maintaining a fork you will
/// need to regenerate the Secrets file with your PostHog server and API key before enabling.
let analyticsConfiguration = AnalyticsConfiguration(isEnabled: InfoPlistReader.main.bundleIdentifier.starts(with: "io.element."),
host: Secrets.postHogHost,
apiKey: Secrets.postHogAPIKey,
termsURL: "https://element.io/cookie-policy")
/// The configuration to use for analytics. Set to `nil` to disable analytics.
let analyticsConfiguration: AnalyticsConfiguration? = AnalyticsConfiguration(host: Secrets.postHogHost, apiKey: Secrets.postHogAPIKey)
/// The URL to open with more information about analytics terms. When this is `nil` the "Learn more" link will be hidden.
private(set) var analyticsTermsURL: URL? = "https://element.io/cookie-policy"
/// Whether or not there the app is able ask for user consent to enable analytics or sentry reporting.
var canPromptForAnalytics: Bool { analyticsConfiguration != nil || bugReportSentryURL != nil }
/// Whether the user has opted in to send analytics.
@UserPreference(key: UserDefaultsKeys.analyticsConsentState, defaultValue: AnalyticsConsentState.unknown, storageType: .userDefaults(store))
@@ -68,7 +68,8 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
// MARK: - Private
private func showStartScreen() {
let parameters = AuthenticationStartScreenParameters(webRegistrationEnabled: appSettings.webRegistrationEnabled)
let parameters = AuthenticationStartScreenParameters(webRegistrationEnabled: appSettings.webRegistrationEnabled,
isBugReportServiceEnabled: bugReportService.isEnabled)
let coordinator = AuthenticationStartScreenCoordinator(parameters: parameters)
coordinator.actions
@@ -379,7 +379,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
}
private func presentAnalyticsPromptScreen() {
let coordinator = AnalyticsPromptScreenCoordinator(analytics: analyticsService, termsURL: appSettings.analyticsConfiguration.termsURL)
let coordinator = AnalyticsPromptScreenCoordinator(analytics: analyticsService, termsURL: appSettings.analyticsTermsURL)
coordinator.actions
.sink { [weak self] action in
@@ -86,7 +86,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
navigationStackCoordinator = NavigationStackCoordinator()
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: .init(userSession: parameters.userSession,
appSettings: parameters.appSettings))
appSettings: parameters.appSettings,
isBugReportServiceEnabled: parameters.bugReportService.isEnabled))
settingsScreenCoordinator.actions
.sink { [weak self] action in
@@ -0,0 +1,20 @@
//
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
extension BugReportServiceMock {
struct Configuration {
var isEnabled = true
}
convenience init(_ configuration: Configuration) {
self.init()
isEnabled = configuration.isEnabled
}
}
@@ -2130,6 +2130,11 @@ class BannedRoomProxyMock: BannedRoomProxyProtocol, @unchecked Sendable {
}
}
class BugReportServiceMock: BugReportServiceProtocol, @unchecked Sendable {
var isEnabled: Bool {
get { return underlyingIsEnabled }
set(value) { underlyingIsEnabled = value }
}
var underlyingIsEnabled: Bool!
var crashedLastRun: Bool {
get { return underlyingCrashedLastRun }
set(value) { underlyingCrashedLastRun = value }
@@ -13,17 +13,21 @@ import CoreLocation
/// [FORKING.md](https://github.com/element-hq/element-x-ios/blob/develop/docs/FORKING.md#setup-the-location-sharing)
struct MapTilerConfiguration {
let baseURL: URL
let apiKey: String
/// The API key for fetching map tiles. When not set, location sharing will be disabled and
/// any received locations will be shown in the timeline with a generic blurred map image.
let apiKey: String?
/// A MapLibre style ID for a light-mode map.
let lightStyleID: String
/// A MapLibre style ID for a dark-mode map.
let darkStyleID: String
var isEnabled: Bool { apiKey != nil }
}
extension MapTilerConfiguration: MapTilerURLBuilderProtocol {
func dynamicMapURL(for style: MapTilerStyle) -> URL? {
var url = makeNewURL(for: style)
url.appendPathComponent("style.json", conformingTo: .json)
url?.appendPathComponent("style.json", conformingTo: .json)
return url
}
@@ -33,20 +37,22 @@ extension MapTilerConfiguration: MapTilerURLBuilderProtocol {
size: CGSize,
attribution: MapTilerAttributionPlacement) -> URL? {
var url = makeNewURL(for: style)
url.appendPathComponent(String(format: "static/%f,%f,%f/%dx%d@2x.png",
coordinates.longitude,
coordinates.latitude,
zoomLevel,
Int(size.width),
Int(size.height)),
conformingTo: .png)
url.append(queryItems: [.init(name: "attribution", value: attribution.rawValue)])
url?.appendPathComponent(String(format: "static/%f,%f,%f/%dx%d@2x.png",
coordinates.longitude,
coordinates.latitude,
zoomLevel,
Int(size.width),
Int(size.height)),
conformingTo: .png)
url?.append(queryItems: [.init(name: "attribution", value: attribution.rawValue)])
return url
}
// MARK: Private
private func makeNewURL(for style: MapTilerStyle) -> URL {
private func makeNewURL(for style: MapTilerStyle) -> URL? {
guard let apiKey else { return nil }
var url: URL = baseURL
url.appendPathComponent(styleID(for: style), conformingTo: .item)
url.append(queryItems: [URLQueryItem(name: "key", value: apiKey)])
@@ -55,7 +55,7 @@ private struct VisualListItemLabelStyle: LabelStyle {
// MARK: - Previews
struct VisualListItem_Previews: PreviewProvider, TestablePreview {
static let strings = AnalyticsPromptScreenStrings(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
static let strings = AnalyticsPromptScreenStrings(termsURL: ServiceLocator.shared.settings.analyticsTermsURL)
@ViewBuilder
static var testImage1: some View {
@@ -10,6 +10,7 @@ import SwiftUI
struct AuthenticationStartScreenParameters {
let webRegistrationEnabled: Bool
let isBugReportServiceEnabled: Bool
}
final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
@@ -22,7 +23,8 @@ final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
}
init(parameters: AuthenticationStartScreenParameters) {
viewModel = AuthenticationStartScreenViewModel(webRegistrationEnabled: parameters.webRegistrationEnabled)
viewModel = AuthenticationStartScreenViewModel(webRegistrationEnabled: parameters.webRegistrationEnabled,
isBugReportServiceEnabled: parameters.isBugReportServiceEnabled)
}
// MARK: - Public
@@ -26,6 +26,7 @@ enum AuthenticationStartScreenViewModelAction {
struct AuthenticationStartScreenViewState: BindableState {
let isWebRegistrationEnabled: Bool
let isQRCodeLoginEnabled: Bool
let isBugReportServiceEnabled: Bool
}
enum AuthenticationStartScreenViewAction {
@@ -17,9 +17,10 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
actionsSubject.eraseToAnyPublisher()
}
init(webRegistrationEnabled: Bool) {
init(webRegistrationEnabled: Bool, isBugReportServiceEnabled: Bool) {
super.init(initialViewState: AuthenticationStartScreenViewState(isWebRegistrationEnabled: webRegistrationEnabled,
isQRCodeLoginEnabled: !ProcessInfo.processInfo.isiOSAppOnMac))
isQRCodeLoginEnabled: !ProcessInfo.processInfo.isiOSAppOnMac,
isBugReportServiceEnabled: isBugReportServiceEnabled))
}
override func process(viewAction: AuthenticationStartScreenViewAction) {
@@ -34,15 +34,17 @@ struct AuthenticationStartScreen: View {
}
.frame(maxHeight: .infinity)
.safeAreaInset(edge: .bottom) {
Button {
context.send(viewAction: .reportProblem)
} label: {
Text(L10n.commonReportAProblem)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.padding(.bottom)
if context.viewState.isBugReportServiceEnabled {
Button {
context.send(viewAction: .reportProblem)
} label: {
Text(L10n.commonReportAProblem)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.padding(.bottom)
}
.frame(width: geometry.size.width)
}
.frame(width: geometry.size.width)
}
}
.navigationBarHidden(true)
@@ -116,9 +118,18 @@ struct AuthenticationStartScreen: View {
// MARK: - Previews
struct AuthenticationStartScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = AuthenticationStartScreenViewModel(webRegistrationEnabled: true)
static let viewModel = makeViewModel()
static let bugReportDisabledViewModel = makeViewModel(isBugReportServiceEnabled: false)
static var previews: some View {
AuthenticationStartScreen(context: viewModel.context)
.previewDisplayName("Default")
AuthenticationStartScreen(context: bugReportDisabledViewModel.context)
.previewDisplayName("Bug report disabled")
}
static func makeViewModel(isBugReportServiceEnabled: Bool = true) -> AuthenticationStartScreenViewModel {
AuthenticationStartScreenViewModel(webRegistrationEnabled: true,
isBugReportServiceEnabled: isBugReportServiceEnabled)
}
}
@@ -141,7 +141,7 @@ struct BugReportScreen_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
NavigationStack {
let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
BugReportScreen(context: BugReportScreenViewModel(bugReportService: BugReportServiceMock(),
BugReportScreen(context: BugReportScreenViewModel(bugReportService: BugReportServiceMock(.init()),
clientProxy: clientProxy,
screenshot: nil,
isModallyPresented: false).context)
@@ -150,7 +150,7 @@ struct BugReportScreen_Previews: PreviewProvider, TestablePreview {
NavigationStack {
let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
BugReportScreen(context: BugReportScreenViewModel(bugReportService: BugReportServiceMock(),
BugReportScreen(context: BugReportScreenViewModel(bugReportService: BugReportServiceMock(.init()),
clientProxy: clientProxy,
screenshot: Asset.Images.appLogo.image,
isModallyPresented: false).context)
@@ -87,7 +87,9 @@ final class HomeScreenCoordinator: CoordinatorProtocol {
func start() {
#if !DEBUG
if bugReportService.crashedLastRun {
// Note: bugReportService.isEnabled doesn't determine if a user has opted in to Analytics/Sentry.
// Therefore we use lastCrashEventID as this will only be set if we have crash ID from Sentry.
if bugReportService.crashedLastRun, bugReportService.lastCrashEventID != nil {
viewModel.presentCrashedLastRunAlert()
}
#endif
@@ -22,7 +22,7 @@ final class AnalyticsPromptScreenCoordinator: CoordinatorProtocol {
actionsSubject.eraseToAnyPublisher()
}
init(analytics: AnalyticsService, termsURL: URL) {
init(analytics: AnalyticsService, termsURL: URL?) {
self.analytics = analytics
viewModel = AnalyticsPromptScreenViewModel(termsURL: termsURL)
}
@@ -33,17 +33,21 @@ struct AnalyticsPromptScreenStrings {
let point2 = L10n.screenAnalyticsPromptThirdPartySharing
let point3 = L10n.screenAnalyticsPromptSettings
init(termsURL: URL) {
init(termsURL: URL?) {
let content = AttributedString(L10n.screenAnalyticsPromptHelpUsImprove)
// Create the 'read terms' with a placeholder.
let linkPlaceholder = "{link}"
var readTerms = AttributedString(L10n.screenAnalyticsSettingsReadTerms(linkPlaceholder))
var linkString = AttributedString(L10n.screenAnalyticsSettingsReadTermsContentLink)
linkString.link = termsURL
linkString.bold()
readTerms.replace(linkPlaceholder, with: linkString)
optInContent = content + "\n\n" + readTerms
if let termsURL {
// Create the 'read terms' with a placeholder.
let linkPlaceholder = "{link}"
var readTerms = AttributedString(L10n.screenAnalyticsSettingsReadTerms(linkPlaceholder))
var linkString = AttributedString(L10n.screenAnalyticsSettingsReadTermsContentLink)
linkString.link = termsURL
linkString.bold()
readTerms.replace(linkPlaceholder, with: linkString)
optInContent = content + "\n\n" + readTerms
} else {
optInContent = content
}
}
}
@@ -18,7 +18,7 @@ class AnalyticsPromptScreenViewModel: AnalyticsPromptScreenViewModelType, Analyt
}
/// Initialize a view model with the specified prompt type and app display name.
init(termsURL: URL) {
init(termsURL: URL?) {
let promptStrings = AnalyticsPromptScreenStrings(termsURL: termsURL)
super.init(initialViewState: AnalyticsPromptScreenViewState(strings: promptStrings))
}
@@ -91,8 +91,17 @@ struct AnalyticsPromptScreen: View {
// MARK: - Previews
struct AnalyticsPromptScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = AnalyticsPromptScreenViewModel(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
static let viewModel = makeViewModel()
static let noTermsViewModel = makeViewModel(showTerms: false)
static var previews: some View {
AnalyticsPromptScreen(context: viewModel.context)
.previewDisplayName("Default")
AnalyticsPromptScreen(context: noTermsViewModel.context)
.previewDisplayName("No terms")
}
static func makeViewModel(showTerms: Bool = true) -> AnalyticsPromptScreenViewModel {
AnalyticsPromptScreenViewModel(termsURL: showTerms ? ServiceLocator.shared.settings.analyticsTermsURL : nil)
}
}
@@ -74,6 +74,7 @@ struct ComposerToolbarViewState: BindableState {
var audioRecorderState: AudioRecorderState
var isRoomEncrypted: Bool
var isLocationSharingEnabled: Bool
var bindings: ComposerToolbarViewStateBindings
@@ -50,6 +50,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
completionSuggestionService: CompletionSuggestionServiceProtocol,
mediaProvider: MediaProviderProtocol,
mentionDisplayHelper: MentionDisplayHelper,
appSettings: AppSettings,
analyticsService: AnalyticsService,
composerDraftService: ComposerDraftServiceProtocol) {
self.initialText = initialText
@@ -65,6 +66,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
super.init(initialViewState: ComposerToolbarViewState(audioPlayerState: .init(id: .recorderPreview, title: L10n.commonVoiceMessage, duration: 0),
audioRecorderState: .init(),
isRoomEncrypted: roomProxy.infoPublisher.value.isEncrypted,
isLocationSharingEnabled: appSettings.mapTilerConfiguration.isEnabled,
bindings: .init()),
mediaProvider: mediaProvider)
@@ -319,6 +319,7 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions)),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: ComposerDraftServiceMock())
@@ -369,6 +370,7 @@ extension ComposerToolbar {
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: ComposerDraftServiceMock())
model.state.composerEmpty = focused
@@ -387,6 +389,7 @@ extension ComposerToolbar {
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: ComposerDraftServiceMock())
model.state.composerEmpty = focused
@@ -405,6 +408,7 @@ extension ComposerToolbar {
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: ComposerDraftServiceMock())
model.state.composerMode = .recordVoiceMessage(state: AudioRecorderState())
@@ -424,6 +428,7 @@ extension ComposerToolbar {
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: ComposerDraftServiceMock())
model.state.composerMode = .previewVoiceMessage(state: AudioPlayerState(id: .recorderPreview,
@@ -446,6 +451,7 @@ extension ComposerToolbar {
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: ComposerDraftServiceMock())
model.state.composerMode = isLoading ? .reply(eventID: UUID().uuidString,
@@ -470,6 +476,7 @@ extension ComposerToolbar {
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: ComposerDraftServiceMock())
model.state.canSend = false
@@ -47,12 +47,14 @@ struct RoomAttachmentPicker: View {
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerPoll)
Button {
context.send(viewAction: .attach(.location))
} label: {
Label(L10n.screenRoomAttachmentSourceLocation, icon: \.locationPin)
if context.viewState.isLocationSharingEnabled {
Button {
context.send(viewAction: .attach(.location))
} label: {
Label(L10n.screenRoomAttachmentSourceLocation, icon: \.locationPin)
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerLocation)
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerLocation)
Button {
context.send(viewAction: .attach(.file))
@@ -91,6 +93,7 @@ struct RoomAttachmentPicker_Previews: PreviewProvider, TestablePreview {
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: ComposerDraftServiceMock())
@@ -71,7 +71,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
mediaProvider: parameters.mediaProvider,
ongoingCallRoomIDPublisher: parameters.ongoingCallRoomIDPublisher,
appMediator: parameters.appMediator,
appSettings: ServiceLocator.shared.settings,
appSettings: parameters.appSettings,
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
@@ -99,6 +99,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
completionSuggestionService: parameters.completionSuggestionService,
mediaProvider: parameters.mediaProvider,
mentionDisplayHelper: ComposerMentionDisplayHelper(timelineContext: timelineViewModel.context),
appSettings: parameters.appSettings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: parameters.composerDraftService)
self.composerViewModel = composerViewModel
@@ -24,17 +24,21 @@ enum AnalyticsSettingsScreenViewAction {
struct AnalyticsSettingsScreenStrings {
let sectionFooter: AttributedString
init(termsURL: URL) {
init(termsURL: URL?) {
let content = AttributedString(L10n.screenAnalyticsPromptHelpUsImprove)
// Create the 'read terms' with a placeholder.
let linkPlaceholder = "{link}"
var readTerms = AttributedString(L10n.screenAnalyticsSettingsReadTerms(linkPlaceholder))
var linkString = AttributedString(L10n.screenAnalyticsSettingsReadTermsContentLink)
linkString.link = termsURL
linkString.bold()
readTerms.replace(linkPlaceholder, with: linkString)
sectionFooter = content + "\n\n" + readTerms
if let termsURL {
// Create the 'read terms' with a placeholder.
let linkPlaceholder = "{link}"
var readTerms = AttributedString(L10n.screenAnalyticsSettingsReadTerms(linkPlaceholder))
var linkString = AttributedString(L10n.screenAnalyticsSettingsReadTermsContentLink)
linkString.link = termsURL
linkString.bold()
readTerms.replace(linkPlaceholder, with: linkString)
sectionFooter = content + "\n\n" + readTerms
} else {
sectionFooter = content
}
}
}
@@ -16,7 +16,7 @@ class AnalyticsSettingsScreenViewModel: AnalyticsSettingsScreenViewModelType, An
init(appSettings: AppSettings, analytics: AnalyticsService) {
self.analytics = analytics
let strings = AnalyticsSettingsScreenStrings(termsURL: appSettings.analyticsConfiguration.termsURL)
let strings = AnalyticsSettingsScreenStrings(termsURL: appSettings.analyticsTermsURL)
let bindings = AnalyticsSettingsScreenViewStateBindings(enableAnalytics: analytics.isEnabled)
let state = AnalyticsSettingsScreenViewState(strings: strings, bindings: bindings)
@@ -11,6 +11,7 @@ import SwiftUI
struct SettingsScreenCoordinatorParameters {
let userSession: UserSessionProtocol
let appSettings: AppSettings
let isBugReportServiceEnabled: Bool
}
enum SettingsScreenCoordinatorAction {
@@ -43,7 +44,9 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
// MARK: - Setup
init(parameters: SettingsScreenCoordinatorParameters) {
viewModel = SettingsScreenViewModel(userSession: parameters.userSession)
viewModel = SettingsScreenViewModel(userSession: parameters.userSession,
appSettings: parameters.appSettings,
isBugReportServiceEnabled: parameters.isBugReportServiceEnabled)
viewModel.actions
.sink { [weak self] action in
@@ -44,6 +44,9 @@ struct SettingsScreenViewState: BindableState {
var showSecuritySectionBadge = false
var showBlockedUsers = false
let showAnalyticsSettings: Bool
let isBugReportServiceEnabled: Bool
var bindings = SettingsScreenViewStateBindings()
}
@@ -17,11 +17,13 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
actionsSubject.eraseToAnyPublisher()
}
init(userSession: UserSessionProtocol) {
init(userSession: UserSessionProtocol, appSettings: AppSettings, isBugReportServiceEnabled: Bool) {
super.init(initialViewState: .init(deviceID: userSession.clientProxy.deviceID,
userID: userSession.clientProxy.userID,
showAccountDeactivation: userSession.clientProxy.canDeactivateAccount,
showDeveloperOptions: AppSettings.isDevelopmentBuild),
showDeveloperOptions: AppSettings.isDevelopmentBuild,
showAnalyticsSettings: appSettings.canPromptForAnalytics,
isBugReportServiceEnabled: isBugReportServiceEnabled),
mediaProvider: userSession.mediaProvider)
userSession.clientProxy.userAvatarURLPublisher
@@ -140,19 +140,23 @@ struct SettingsScreen: View {
})
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.about)
ListRow(label: .default(title: L10n.commonReportAProblem,
icon: \.chatProblem),
kind: .navigationLink {
context.send(viewAction: .reportBug)
})
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.reportBug)
if context.viewState.isBugReportServiceEnabled {
ListRow(label: .default(title: L10n.commonReportAProblem,
icon: \.chatProblem),
kind: .navigationLink {
context.send(viewAction: .reportBug)
})
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.reportBug)
}
ListRow(label: .default(title: L10n.commonAnalytics,
icon: \.chart),
kind: .navigationLink {
context.send(viewAction: .analytics)
})
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics)
if context.viewState.showAnalyticsSettings {
ListRow(label: .default(title: L10n.commonAnalytics,
icon: \.chart),
kind: .navigationLink {
context.send(viewAction: .analytics)
})
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics)
}
ListRow(label: .default(title: L10n.commonAdvancedSettings,
icon: \.settings),
@@ -230,18 +234,32 @@ struct SettingsScreen: View {
// MARK: - Previews
struct SettingsScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = {
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com",
deviceID: "AAAAAAAAAAA"))))
return SettingsScreenViewModel(userSession: userSession)
}()
static let viewModel = makeViewModel()
static let bugReportDisabledViewModel = makeViewModel(isBugReportServiceEnabled: false)
static var previews: some View {
NavigationStack {
SettingsScreen(context: viewModel.context)
.snapshotPreferences(expect: viewModel.context.$viewState.map { state in
state.accountSessionsListURL != nil
})
}
.snapshotPreferences(expect: viewModel.context.$viewState.map { state in
state.accountSessionsListURL != nil
})
.previewDisplayName("Default")
NavigationStack {
SettingsScreen(context: bugReportDisabledViewModel.context)
}
.snapshotPreferences(expect: bugReportDisabledViewModel.context.$viewState.map { state in
state.accountSessionsListURL != nil
})
.previewDisplayName("Bug report disabled")
}
static func makeViewModel(isBugReportServiceEnabled: Bool = true) -> SettingsScreenViewModel {
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com",
deviceID: "AAAAAAAAAAA"))))
return SettingsScreenViewModel(userSession: userSession,
appSettings: ServiceLocator.shared.settings,
isBugReportServiceEnabled: isBugReportServiceEnabled)
}
}
@@ -123,7 +123,7 @@ struct TimelineViewState: BindableState {
var emojiProvider: EmojiProviderProtocol
var mapURLBuilder: MapTilerURLBuilderProtocol
var mapTilerConfiguration: MapTilerConfiguration
var bindings: TimelineViewStateBindings
}
@@ -95,7 +95,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
hideTimelineMedia: appSettings.hideTimelineMedia,
pinnedEventIDs: roomProxy.infoPublisher.value.pinnedEventIDs,
emojiProvider: emojiProvider,
mapURLBuilder: appSettings.mapTilerConfiguration,
mapTilerConfiguration: appSettings.mapTilerConfiguration,
bindings: .init(reactionsCollapsed: [:])),
mediaProvider: mediaProvider)
@@ -17,6 +17,7 @@ struct LocationRoomTimelineView: View {
.accessibilityElement(children: .ignore)
.accessibilityLabel(accessibilityLabel)
.onTapGesture {
guard context.viewState.mapTilerConfiguration.isEnabled else { return }
context.send(viewAction: .mediaTapped(itemID: timelineItem.id))
}
}
@@ -30,7 +31,7 @@ struct LocationRoomTimelineView: View {
.frame(maxWidth: mapAspectRatio * mapMaxHeight, alignment: .leading)
MapLibreStaticMapView(geoURI: geoURI,
mapURLBuilder: context.viewState.mapURLBuilder,
mapURLBuilder: context.viewState.mapTilerConfiguration,
mapSize: .init(width: mapAspectRatio * mapMaxHeight, height: mapMaxHeight)) {
LocationMarkerView()
}
@@ -9,12 +9,8 @@ import Foundation
/// A type that represents how to set up the analytics module in the app.
struct AnalyticsConfiguration {
/// Whether or not analytics should be enabled.
let isEnabled: Bool
/// The host to use for PostHog analytics.
let host: String
/// The public key for submitting analytics.
let apiKey: String
/// The URL to open with more information about analytics terms.
let termsURL: URL
}
@@ -39,7 +39,7 @@ class AnalyticsService {
/// Whether to show the user the analytics opt in prompt.
var shouldShowAnalyticsPrompt: Bool {
// Only show the prompt once, and when analytics are enabled in BuildSettings.
appSettings.analyticsConsentState == .unknown && appSettings.analyticsConfiguration.isEnabled
appSettings.analyticsConsentState == .unknown && appSettings.canPromptForAnalytics
}
var isEnabled: Bool {
@@ -65,9 +65,9 @@ class AnalyticsService {
/// Starts the analytics client if the user has opted in, otherwise does nothing.
func startIfEnabled() {
guard isEnabled, !client.isRunning else { return }
guard isEnabled, !client.isRunning, let configuration = appSettings.analyticsConfiguration else { return }
client.start(analyticsConfiguration: appSettings.analyticsConfiguration)
client.start(analyticsConfiguration: configuration)
// Sanity check in case something went wrong.
guard client.isRunning else { return }
@@ -9,8 +9,6 @@ import PostHog
extension PostHogConfig {
static func standard(analyticsConfiguration: AnalyticsConfiguration) -> PostHogConfig? {
guard analyticsConfiguration.isEnabled else { return nil }
let postHogConfiguration = PostHogConfig(apiKey: analyticsConfiguration.apiKey, host: analyticsConfiguration.host)
// We capture screens manually
postHogConfiguration.captureScreenViews = false
@@ -12,8 +12,8 @@ import Sentry
import UIKit
class BugReportService: NSObject, BugReportServiceProtocol {
private let baseURL: URL
private let applicationId: String
private let baseURL: URL?
private let applicationID: String
private let sdkGitSHA: String
private let maxUploadSize: Int
private let session: URLSession
@@ -23,16 +23,17 @@ class BugReportService: NSObject, BugReportServiceProtocol {
private let progressSubject = PassthroughSubject<Double, Never>()
private var cancellables = Set<AnyCancellable>()
var isEnabled: Bool { baseURL != nil }
var lastCrashEventID: String?
init(withBaseURL baseURL: URL,
applicationId: String,
init(baseURL: URL?,
applicationID: String,
sdkGitSHA: String,
maxUploadSize: Int,
session: URLSession = .shared,
appHooks: AppHooks) {
self.baseURL = baseURL
self.applicationId = applicationId
self.applicationID = applicationID
self.sdkGitSHA = sdkGitSHA
self.maxUploadSize = maxUploadSize
self.session = session
@@ -49,6 +50,10 @@ class BugReportService: NSObject, BugReportServiceProtocol {
// swiftlint:disable:next cyclomatic_complexity
func submitBugReport(_ bugReport: BugReport,
progressListener: CurrentValueSubject<Double, Never>) async -> Result<SubmitBugReportResponse, BugReportServiceError> {
guard let baseURL else {
fatalError("No bug report URL set, the screen should not be shown in this case.")
}
let bugReport = appHooks.bugReportHook.update(bugReport)
var params = [
@@ -153,7 +158,7 @@ class BugReportService: NSObject, BugReportServiceProtocol {
let (localTime, utcTime) = localAndUTCTime(for: Date())
return [
MultipartFormData(key: "user_agent", type: .text(value: "iOS")),
MultipartFormData(key: "app", type: .text(value: applicationId)),
MultipartFormData(key: "app", type: .text(value: applicationID)),
MultipartFormData(key: "version", type: .text(value: InfoPlistReader.main.bundleShortVersionString)),
MultipartFormData(key: "build", type: .text(value: InfoPlistReader.main.bundleVersion)),
MultipartFormData(key: "sdk_sha", type: .text(value: sdkGitSHA)),
@@ -44,6 +44,7 @@ enum BugReportServiceError: LocalizedError {
// sourcery: AutoMockable
protocol BugReportServiceProtocol: AnyObject {
var isEnabled: Bool { get }
var crashedLastRun: Bool { get }
var lastCrashEventID: String? { get set }
@@ -37,7 +37,7 @@ class UITestsAppCoordinator: AppCoordinatorProtocol, SecureWindowManagerDelegate
AppSettings.configureWithSuiteName("io.element.elementx.uitests")
AppSettings.resetAllSettings()
ServiceLocator.shared.register(appSettings: AppSettings())
ServiceLocator.shared.register(bugReportService: BugReportServiceMock())
ServiceLocator.shared.register(bugReportService: BugReportServiceMock(.init()))
let analyticsClient = AnalyticsClientMock()
analyticsClient.isRunning = false
@@ -124,7 +124,7 @@ class MockScreen: Identifiable {
case .authenticationFlow:
let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: AuthenticationService.mock,
qrCodeLoginService: QRCodeLoginServiceMock(),
bugReportService: BugReportServiceMock(),
bugReportService: BugReportServiceMock(.init()),
navigationRootCoordinator: navigationRootCoordinator,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
@@ -228,7 +228,7 @@ class MockScreen: Identifiable {
let navigationStackCoordinator = NavigationStackCoordinator()
let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
let coordinator = BugReportScreenCoordinator(parameters: .init(bugReportService: BugReportServiceMock(),
let coordinator = BugReportScreenCoordinator(parameters: .init(bugReportService: BugReportServiceMock(.init()),
userSession: userSession,
userIndicatorController: nil,
screenshot: nil,
@@ -557,7 +557,7 @@ class MockScreen: Identifiable {
navigationRootCoordinator: navigationRootCoordinator,
appLockService: AppLockService(keychainController: KeychainControllerMock(),
appSettings: ServiceLocator.shared.settings),
bugReportService: BugReportServiceMock(),
bugReportService: BugReportServiceMock(.init()),
elementCallService: ElementCallServiceMock(.init()),
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
appMediator: appMediator,
@@ -711,7 +711,7 @@ class MockScreen: Identifiable {
navigationRootCoordinator: navigationRootCoordinator,
appLockService: AppLockService(keychainController: KeychainControllerMock(),
appSettings: ServiceLocator.shared.settings),
bugReportService: BugReportServiceMock(),
bugReportService: BugReportServiceMock(.init()),
elementCallService: ElementCallServiceMock(.init()),
timelineControllerFactory: TimelineControllerFactoryMock(.init(timelineController: timelineController)),
appMediator: AppMediatorMock.default,
@@ -17,7 +17,7 @@ class UnitTestsAppCoordinator: AppCoordinatorProtocol {
AppSettings.configureWithSuiteName("io.element.elementx.unittests")
AppSettings.resetAllSettings()
ServiceLocator.shared.register(appSettings: AppSettings())
ServiceLocator.shared.register(bugReportService: BugReportServiceMock())
ServiceLocator.shared.register(bugReportService: BugReportServiceMock(.init()))
let analyticsClient = AnalyticsClientMock()
analyticsClient.isRunning = false
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b6fcab1ccc72812d59a6b927c7731b7c1cae82b457a86ed6530cf610e9ae55f1
size 509252
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:725a62a99cc8483ecb291544dd58028b9e967e8ca3c63a6b64a8cfc1def27800
size 529773
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ae449487cd4e9b77c360a17f57e117a7ec3048a88f33d53f72fbcc22e6c666e4
size 316319
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c87868f81842fdeeac6f51a73334b1cc1f9c48004256f9e9a50dbaa507c5092b
size 332515
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:816f085c82fc0866cc1052e8b16bc6cb035d5f69cdc8388dd44d8a5ced97b34e
size 109090
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba61a003238a0fb7b3188dca3f5735c386c9d2bd6488ae15d31376d3b1899a73
size 126888
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d7b8b73c30044017dc8db3f5b540011fb9e502d68beabfae0f85daede922f342
size 521551
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3074c78aa668a0b9f194e85bbd41af06985c831e6680f4d7bc8c131f0acdb185
size 544472
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:82aceb5956ac84373d141855acaa16a962034001d2b4a01c4bd30a347505e2f8
size 160152
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ce61dcd6449607c67a4aa12d96140e1159afce33481351a16e7af9f90c5e932
size 166763
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:967c85e91f106b89c44f17facb9ac369bbca2e19e33b1cd6875108aba2fd5b49
size 106646
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eab5bec1eb422c8918f0e7c634d2b1cd3b1190dc001547db99159395241fea3e
size 123586
+6 -6
View File
@@ -155,11 +155,11 @@ class AnalyticsTests: XCTestCase {
XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should have been updated.")
}
func testSendingUserProperties() {
func testSendingUserProperties() throws {
// Given a client with user properties set
let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock))
client.start(analyticsConfiguration: appSettings.analyticsConfiguration)
try client.start(analyticsConfiguration: XCTUnwrap(appSettings.analyticsConfiguration))
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging,
numFavouriteRooms: nil,
@@ -204,10 +204,10 @@ class AnalyticsTests: XCTestCase {
XCTAssertTrue(ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt)
}
func testSendingAndUpdatingSuperProperties() {
func testSendingAndUpdatingSuperProperties() throws {
// Given a client with user properties set
let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock))
client.start(analyticsConfiguration: appSettings.analyticsConfiguration)
try client.start(analyticsConfiguration: XCTUnwrap(appSettings.analyticsConfiguration))
client.updateSuperProperties(
AnalyticsEvent.SuperProperties(appPlatform: .EXI,
@@ -264,7 +264,7 @@ class AnalyticsTests: XCTestCase {
XCTAssertEqual(capturedEvent2?.properties?["cryptoSDKVersion"] as? String, "001")
}
func testShouldNotReportIfNotStarted() {
func testShouldNotReportIfNotStarted() throws {
// Given a client with user properties set
let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock))
@@ -291,7 +291,7 @@ class AnalyticsTests: XCTestCase {
XCTAssertEqual(posthogMock.capturePropertiesUserPropertiesCalled, false)
// start now
client.start(analyticsConfiguration: appSettings.analyticsConfiguration)
try client.start(analyticsConfiguration: XCTUnwrap(appSettings.analyticsConfiguration))
XCTAssertEqual(posthogMock.optInCalled, true)
client.capture(someEvent)
+16 -4
View File
@@ -41,18 +41,30 @@ class BugReportServiceTests: XCTestCase {
}
func testInitialStateWithRealService() throws {
let service = BugReportService(withBaseURL: "https://www.example.com",
applicationId: "mock_app_id",
let service = BugReportService(baseURL: "https://www.example.com",
applicationID: "mock_app_id",
sdkGitSHA: "1234",
maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize,
session: .mock,
appHooks: AppHooks())
XCTAssertTrue(service.isEnabled)
XCTAssertFalse(service.crashedLastRun)
}
func testInitialStateWithRealServiceAndNoURL() throws {
let service = BugReportService(baseURL: nil,
applicationID: "mock_app_id",
sdkGitSHA: "1234",
maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize,
session: .mock,
appHooks: AppHooks())
XCTAssertFalse(service.isEnabled)
XCTAssertFalse(service.crashedLastRun)
}
@MainActor func testSubmitBugReportWithRealService() async throws {
let service = BugReportService(withBaseURL: "https://www.example.com",
applicationId: "mock_app_id",
let service = BugReportService(baseURL: "https://www.example.com",
applicationID: "mock_app_id",
sdkGitSHA: "1234",
maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize,
session: .mock,
@@ -100,6 +100,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
completionSuggestionService: mockCompletionSuggestionService,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: draftServiceMock)
@@ -684,6 +685,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
completionSuggestionService: mockCompletionSuggestionService,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: draftServiceMock)
@@ -727,6 +729,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
completionSuggestionService: mockCompletionSuggestionService,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: draftServiceMock)
@@ -760,6 +763,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
completionSuggestionService: mockCompletionSuggestionService,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: draftServiceMock)
@@ -794,6 +798,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
completionSuggestionService: completionSuggestionServiceMock,
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: draftServiceMock)
viewModel.context.composerFormattingEnabled = true
@@ -51,4 +51,24 @@ final class MapTilerURLBuilderTests: XCTestCase {
let expectedURL: URL = "http://www.foo.com/dea61faf-292b-4774-9660-58fcef89a7f3/style.json?key=some_key"
XCTAssertEqual(url, expectedURL)
}
func testNilAPIKey() {
let configuration = MapTilerConfiguration(baseURL: Self.baseURL,
apiKey: nil,
lightStyleID: Self.lightStyleID,
darkStyleID: Self.darkStyleID)
XCTAssertFalse(configuration.isEnabled)
builder = configuration
let staticMapURL = builder.staticMapURL(for: .dark,
coordinates: .init(latitude: 1, longitude: 2),
zoomLevel: 5,
size: .init(width: 300, height: 200),
attribution: .topLeft)
XCTAssertNil(staticMapURL)
let dynamicMapURL = builder.dynamicMapURL(for: .light)
XCTAssertNil(dynamicMapURL)
}
}
@@ -19,7 +19,9 @@ class SettingsScreenViewModelTests: XCTestCase {
@MainActor override func setUpWithError() throws {
cancellables.removeAll()
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: ""))))
viewModel = SettingsScreenViewModel(userSession: userSession)
viewModel = SettingsScreenViewModel(userSession: userSession,
appSettings: ServiceLocator.shared.settings,
isBugReportServiceEnabled: true)
context = viewModel.context
}
@@ -36,7 +36,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
navigationRootCoordinator: navigationRootCoordinator,
appLockService: AppLockServiceMock(),
bugReportService: BugReportServiceMock(),
bugReportService: BugReportServiceMock(.init()),
elementCallService: ElementCallServiceMock(.init()),
timelineControllerFactory: timelineControllerFactory,
appMediator: AppMediatorMock.default,