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
@@ -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()
}