mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 08:15:24 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ enum AuthenticationStartScreenViewModelAction {
|
||||
struct AuthenticationStartScreenViewState: BindableState {
|
||||
let isWebRegistrationEnabled: Bool
|
||||
let isQRCodeLoginEnabled: Bool
|
||||
let isBugReportServiceEnabled: Bool
|
||||
}
|
||||
|
||||
enum AuthenticationStartScreenViewAction {
|
||||
|
||||
+3
-2
@@ -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) {
|
||||
|
||||
+20
-9
@@ -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
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
|
||||
+14
-10
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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))
|
||||
}
|
||||
|
||||
+10
-1
@@ -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
|
||||
|
||||
+14
-10
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b6fcab1ccc72812d59a6b927c7731b7c1cae82b457a86ed6530cf610e9ae55f1
|
||||
size 509252
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:725a62a99cc8483ecb291544dd58028b9e967e8ca3c63a6b64a8cfc1def27800
|
||||
size 529773
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ae449487cd4e9b77c360a17f57e117a7ec3048a88f33d53f72fbcc22e6c666e4
|
||||
size 316319
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c87868f81842fdeeac6f51a73334b1cc1f9c48004256f9e9a50dbaa507c5092b
|
||||
size 332515
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:816f085c82fc0866cc1052e8b16bc6cb035d5f69cdc8388dd44d8a5ced97b34e
|
||||
size 109090
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ba61a003238a0fb7b3188dca3f5735c386c9d2bd6488ae15d31376d3b1899a73
|
||||
size 126888
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d7b8b73c30044017dc8db3f5b540011fb9e502d68beabfae0f85daede922f342
|
||||
size 521551
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3074c78aa668a0b9f194e85bbd41af06985c831e6680f4d7bc8c131f0acdb185
|
||||
size 544472
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:82aceb5956ac84373d141855acaa16a962034001d2b4a01c4bd30a347505e2f8
|
||||
size 160152
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0ce61dcd6449607c67a4aa12d96140e1159afce33481351a16e7af9f90c5e932
|
||||
size 166763
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:967c85e91f106b89c44f17facb9ac369bbca2e19e33b1cd6875108aba2fd5b49
|
||||
size 106646
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eab5bec1eb422c8918f0e7c634d2b1cd3b1190dc001547db99159395241fea3e
|
||||
size 123586
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user