diff --git a/WireFoundation/Sources/WireFoundation/WirePrimitives/UITestConfig.swift b/WireFoundation/Sources/WireFoundation/WirePrimitives/UITestConfig.swift index ddebd50567a..6e7b6596bfd 100644 --- a/WireFoundation/Sources/WireFoundation/WirePrimitives/UITestConfig.swift +++ b/WireFoundation/Sources/WireFoundation/WirePrimitives/UITestConfig.swift @@ -28,6 +28,14 @@ public struct UITestConfig: Codable { public var isBuildBlacklisted = false + /// When `true`, a triple-tap on the app window triggers the same action as the shake gesture. + /// On XCUITests, shake gesture is not available. + public var useTripleTapForShakeGesture = false + + /// Developer flags to apply at launch, keyed by `DeveloperFlag.rawValue`. + /// Overrides any flags already stored in `UserDefaults`. + public var developerFlags: [String: Bool] = [:] + // MARK: - Init public init() {} diff --git a/WireFoundation/Sources/WireTesting/Utilities/SnapshotHelper.swift b/WireFoundation/Sources/WireTesting/Utilities/SnapshotHelper.swift index 3c8815f33f2..12214868c5c 100644 --- a/WireFoundation/Sources/WireTesting/Utilities/SnapshotHelper.swift +++ b/WireFoundation/Sources/WireTesting/Utilities/SnapshotHelper.swift @@ -34,7 +34,14 @@ public struct SnapshotHelper { private var defaultRecordMode: SnapshotTestingConfiguration.Record? { let ci = ProcessInfo.processInfo.environment["CI"] - return (ci == nil || ci?.isEmpty == true) ? .missing : .never + if let value = ProcessInfo.processInfo.environment["SNAPSHOT_TESTING_RECORD"], + let record = SnapshotTestingConfiguration.Record(rawValue: value) { + return record + } else if ci == nil || ci?.isEmpty == true { + return .missing + } else { + return .never + } } public init() {} @@ -360,7 +367,7 @@ public struct SnapshotHelper { matching value: UIViewController, size: CGSize? = nil, named name: String? = nil, - record recording: Bool = false, + record recording: Bool? = nil, file: StaticString = #filePath, testName: String = #function, safeArea: UIEdgeInsets = .zero, @@ -522,7 +529,7 @@ public struct SnapshotHelper { public func verify( matching value: UIImage, named name: String? = nil, - record recording: Bool = false, + record recording: Bool? = nil, file: StaticString = #filePath, testName: String = #function, line: UInt = #line diff --git a/WireLogging/Sources/WireLogging/LogFilesProviding.swift b/WireLogging/Sources/WireLogging/LogFilesProviding.swift index cd04e07e713..69fafcb1894 100644 --- a/WireLogging/Sources/WireLogging/LogFilesProviding.swift +++ b/WireLogging/Sources/WireLogging/LogFilesProviding.swift @@ -21,28 +21,19 @@ public import Foundation // sourcery: AutoMockable public protocol LogFilesProviding { - /// Generates a zip file containing all log files and returns its data before removing the files - /// - /// - Returns: the log files archive data + /// All log file URLs to include in the archive. + var logFileURLs: [URL] { get } - func generateLogFilesData() throws -> Data - - /// Generates a zip file containing all log files - /// - /// - Returns: the log files archive URL - - func generateLogFilesZip() throws -> URL + /// Returns system and app info, optionally including journal entries for the given user. + func info(selfUserID: UUID?) -> String /// Clears the logs directory. - /// Call once you are done using the URL returned by `generateLogFilesZip` to clean up. - + /// Call once you are done using the URL returned by `CreateDebugReportUseCase` to clean up. func clearLogsDirectory(fileManager: FileManager) throws /// Clears individual log files from their source locations. - func removeLogFiles(fileManager: FileManager) throws - /// Deletes all log-related legacy archives - + /// Deletes all log-related legacy archives. func removeLegacyLogArchives() throws } diff --git a/WireUI/Sources/WireDesign/Appearance/UITabBarController+applyMainTabBarControllerAppearance.swift b/WireUI/Sources/WireDesign/Appearance/UITabBarController+applyMainTabBarControllerAppearance.swift index 6a620a69b79..2efe3f62b3a 100644 --- a/WireUI/Sources/WireDesign/Appearance/UITabBarController+applyMainTabBarControllerAppearance.swift +++ b/WireUI/Sources/WireDesign/Appearance/UITabBarController+applyMainTabBarControllerAppearance.swift @@ -28,10 +28,10 @@ public extension UITabBarController { let tabBarAppearance = UITabBarAppearance() tabBarAppearance.configureWithDefaultBackground() - tabBarAppearance.backgroundColor = ColorTheme.Backgrounds.background + tabBarAppearance.backgroundColor = ColorTheme.Backgrounds.backgroundVariant tabBarAppearance.stackedLayoutAppearance = tabBarItemAppearance - tabBar.backgroundColor = ColorTheme.Backgrounds.background + tabBar.backgroundColor = ColorTheme.Backgrounds.backgroundVariant tabBar.unselectedItemTintColor = ColorTheme.Base.secondaryText tabBar.standardAppearance = tabBarAppearance } diff --git a/WireUI/Sources/WireDesign/Colors/ColorTheme.swift b/WireUI/Sources/WireDesign/Colors/ColorTheme.swift index f337d5f4de8..6c25a3e42fd 100644 --- a/WireUI/Sources/WireDesign/Colors/ColorTheme.swift +++ b/WireUI/Sources/WireDesign/Colors/ColorTheme.swift @@ -201,6 +201,16 @@ public enum ColorTheme { public enum NotificationBadge { public static let fill = ColorTheme.Base.error } + + // MARK: - Wire Design System + + // TODO: [WPB-25347] implement the new color system + public enum Content { + public enum Base { + public static let secondary = UIColor(light: .gray70, dark: .gray60) + } + } + } private extension UIColor { diff --git a/WireUI/Sources/WireDesign/Typography/WireTextStyle.swift b/WireUI/Sources/WireDesign/Typography/WireTextStyle.swift index b7c56cf966e..81928d0d17e 100644 --- a/WireUI/Sources/WireDesign/Typography/WireTextStyle.swift +++ b/WireUI/Sources/WireDesign/Typography/WireTextStyle.swift @@ -20,18 +20,43 @@ public enum WireTextStyle: String, CaseIterable, Sendable { + /// Style iOS & Figma: ? case largeTitle + + /// Style iOS & Figma: Title 3 case h1 + + /// Style iOS & Figma: Title 3 (bold) - Emphasized case h2 + + /// Style iOS & Figma: Headline case h3 + + /// Style iOS & Figma: Subheadline case h4 + + /// Style iOS & Figma: Footnote case h5 + + /// Style iOS & Figma: Body case body1 + + /// Style iOS & Figma: Body 2 (custom) case body2 + + /// Figma: Callout (bold) - Emphasized case body3 + + /// Style iOS & Figma: Caption 1 case subline1 + + /// Style iOS & Figma: Caption 1 (bold) - Emphasized case subline2 + + /// Style iOS & Figma: Button Small (custom) case buttonSmall + + /// Style iOS & Figma: Button Big (custom) case buttonBig } diff --git a/WireUI/Sources/WireLocators/Locators.swift b/WireUI/Sources/WireLocators/Locators.swift index deb307d3d07..af03ab0b336 100644 --- a/WireUI/Sources/WireLocators/Locators.swift +++ b/WireUI/Sources/WireLocators/Locators.swift @@ -89,6 +89,28 @@ public enum Locators { case accountCell case optionsCell + case shareDebugBanner + } + + public enum ShareDebugReportPage: String { + + case actionSheet = "Having trouble?" + case shareViaWireButton = "ShareDebugReportPage.shareViaWireButton" + case sendEmailButton = "ShareDebugReportPage.sendEmailButton" + case shareButton = "ShareDebugReportPage.shareButton" + case cancelButton = "ShareDebugReportPage.cancelButton" + } + + public enum ShareViaWirePage: AutoPrefixedEnum { + + case sendButton + case closeButton + } + + public enum ActivitySheetPage: String { + + case sheet = "ActivityListView" + case saveToFiles = "Save to Files" } public enum AccountSettingsPage: String { diff --git a/WireUI/Sources/WireReusableUIComponents/BlockingActivityIndicator/BlockingActivityIndicator.swift b/WireUI/Sources/WireReusableUIComponents/BlockingActivityIndicator/BlockingActivityIndicator.swift index 2d7cf71ce75..e6c94ac5897 100644 --- a/WireUI/Sources/WireReusableUIComponents/BlockingActivityIndicator/BlockingActivityIndicator.swift +++ b/WireUI/Sources/WireReusableUIComponents/BlockingActivityIndicator/BlockingActivityIndicator.swift @@ -17,24 +17,35 @@ // import SwiftUI +import WireDesign import WireFoundation /// Adds an activity indicator subview to the provided `UIView` instance and disables user interaction. public final class BlockingActivityIndicator { + public enum Style { + /// Full-screen dimmed overlay with a white spinner centered on it. + case fullScreen + /// Full-screen dimmed overlay with a centered white card containing a dark spinner and text. + case card + } + // MARK: - Private Properties private weak var view: UIView? private let accessibilityAnnouncement: String? + private let style: Style // MARK: - Life Cycle public init( view: UIView, - accessibilityAnnouncement: String? + accessibilityAnnouncement: String?, + style: Style = .fullScreen ) { self.view = view self.accessibilityAnnouncement = accessibilityAnnouncement + self.style = style } deinit { @@ -62,7 +73,7 @@ public final class BlockingActivityIndicator { if let accessibilityAnnouncement { UIAccessibility.post(notification: .announcement, argument: accessibilityAnnouncement) } - view?.blockAndStartAnimating(blockingActivityIndicator: self, text: text) + view?.blockAndStartAnimating(blockingActivityIndicator: self, text: text, style: style) } @MainActor @@ -76,6 +87,7 @@ public final class BlockingActivityIndicator { private struct BlockingActivityIndicatorState { var weakReferences = [WeakReference]() private(set) var activityIndicatorView = ProgressSpinner() + var blockingView: UIView? } // MARK: - UIView + BlockingActivityIndicators @@ -84,7 +96,8 @@ private extension UIView { func blockAndStartAnimating( blockingActivityIndicator reference: BlockingActivityIndicator, - text: String + text: String, + style: BlockingActivityIndicator.Style ) { var state: BlockingActivityIndicatorState! = blockingActivityIndicatorState @@ -92,29 +105,64 @@ private extension UIView { if state == nil { state = .init() - // view with dimmed background which swallows touch events + // dim overlay which swallows touch events let blockingView = UIView() + state.blockingView = blockingView blockingView.backgroundColor = .black.withAlphaComponent(0.5) blockingView.isUserInteractionEnabled = true blockingView.translatesAutoresizingMaskIntoConstraints = false addSubview(blockingView) - // activity indicator view - state.activityIndicatorView.color = .white - state.activityIndicatorView.text = text - state.activityIndicatorView.isAnimating = true - state.activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - blockingView.addSubview(state.activityIndicatorView) - NSLayoutConstraint.activate([ blockingView.leadingAnchor.constraint(equalTo: leadingAnchor), blockingView.topAnchor.constraint(equalTo: topAnchor), trailingAnchor.constraint(equalTo: blockingView.trailingAnchor), - bottomAnchor.constraint(equalTo: blockingView.bottomAnchor), - - state.activityIndicatorView.centerXAnchor.constraint(equalTo: blockingView.centerXAnchor), - state.activityIndicatorView.centerYAnchor.constraint(equalTo: blockingView.centerYAnchor) + bottomAnchor.constraint(equalTo: blockingView.bottomAnchor) ]) + + state.activityIndicatorView.text = text + state.activityIndicatorView.isAnimating = true + state.activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false + + switch style { + case .fullScreen: + state.activityIndicatorView.color = .white + blockingView.addSubview(state.activityIndicatorView) + NSLayoutConstraint.activate([ + state.activityIndicatorView.centerXAnchor.constraint(equalTo: blockingView.centerXAnchor), + state.activityIndicatorView.centerYAnchor.constraint(equalTo: blockingView.centerYAnchor) + ]) + + case .card: + state.activityIndicatorView.color = .label + state.activityIndicatorView.textColor = .label + + let card = UIView() + card.backgroundColor = ColorTheme.Backgrounds.surface + card.layer.cornerRadius = 16 + card.layer.masksToBounds = true + card.translatesAutoresizingMaskIntoConstraints = false + blockingView.addSubview(card) + card.addSubview(state.activityIndicatorView) + + // Card prefers 65% of the overlay width (wide enough for label text on iPhone) + // but is capped at 300pt so it stays compact on iPad. + // Spinner uses equalTo horizontal margins so it fills the card — this overrides + // ProgressSpinner.intrinsicContentSize (32pt) which ignores the label width. + let preferredWidth = card.widthAnchor.constraint(equalTo: blockingView.widthAnchor, multiplier: 0.65) + preferredWidth.priority = .defaultHigh + NSLayoutConstraint.activate([ + card.centerXAnchor.constraint(equalTo: blockingView.centerXAnchor), + card.centerYAnchor.constraint(equalTo: blockingView.centerYAnchor), + preferredWidth, + card.widthAnchor.constraint(lessThanOrEqualToConstant: 300), + + state.activityIndicatorView.topAnchor.constraint(equalTo: card.topAnchor, constant: 24), + state.activityIndicatorView.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 24), + card.trailingAnchor.constraint(equalTo: state.activityIndicatorView.trailingAnchor, constant: 24), + card.bottomAnchor.constraint(equalTo: state.activityIndicatorView.bottomAnchor, constant: 24) + ]) + } } // add the reference into the `weakReferences` array @@ -127,7 +175,7 @@ private extension UIView { state.weakReferences = state.weakReferences.filter { $0.reference != nil && $0.reference !== reference } if state.weakReferences.isEmpty { - state.activityIndicatorView.superview!.removeFromSuperview() + state.blockingView?.removeFromSuperview() blockingActivityIndicatorState = nil } } diff --git a/WireUI/Sources/WireReusableUIComponents/ProgressIndicator/ProgressSpinner.swift b/WireUI/Sources/WireReusableUIComponents/ProgressIndicator/ProgressSpinner.swift index 2f44888f6e7..8504ab12001 100644 --- a/WireUI/Sources/WireReusableUIComponents/ProgressIndicator/ProgressSpinner.swift +++ b/WireUI/Sources/WireReusableUIComponents/ProgressIndicator/ProgressSpinner.swift @@ -28,6 +28,10 @@ public final class ProgressSpinner: UIView { didSet { updateSpinnerIcon() } } + public var textColor: UIColor = .white { + didSet { label.textColor = textColor } + } + public var iconSize: CGFloat = 32 { didSet { updateSpinnerIcon() } } @@ -37,6 +41,7 @@ public final class ProgressSpinner: UIView { set { label.text = newValue label.isHidden = newValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + invalidateIntrinsicContentSize() } } @@ -82,6 +87,7 @@ public final class ProgressSpinner: UIView { stackView.translatesAutoresizingMaskIntoConstraints = false addSubview(stackView) + spinner.accessibilityElementsHidden = true spinner.contentMode = .center updateSpinnerIcon() stackView.addArrangedSubview(spinner) @@ -133,7 +139,7 @@ public final class ProgressSpinner: UIView { } public override var intrinsicContentSize: CGSize { - spinner.image?.size ?? super.intrinsicContentSize + stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) } private func startAnimationInternal() { diff --git a/WireUI/Tests/TestPlans/AllTests.xctestplan b/WireUI/Tests/TestPlans/AllTests.xctestplan index 37b7287b66d..4f513475abf 100644 --- a/WireUI/Tests/TestPlans/AllTests.xctestplan +++ b/WireUI/Tests/TestPlans/AllTests.xctestplan @@ -18,6 +18,11 @@ { "key" : "CI", "value" : "${CI}" + }, + { + "enabled" : false, + "key" : "SNAPSHOT_TESTING_RECORD", + "value" : "failed" } ], "language" : "en", diff --git a/WireUI/Tests/WireDesignTests/Resources/ReferenceImages/UITabBarController+applyMainTabBarControllerAppearanceTests/testUIFontContentSizeCategories.accessibilityExtraExtraExtraLarge.png b/WireUI/Tests/WireDesignTests/Resources/ReferenceImages/UITabBarController+applyMainTabBarControllerAppearanceTests/testUIFontContentSizeCategories.accessibilityExtraExtraExtraLarge.png index 6f21914e8d2..dcb69baf11b 100644 --- a/WireUI/Tests/WireDesignTests/Resources/ReferenceImages/UITabBarController+applyMainTabBarControllerAppearanceTests/testUIFontContentSizeCategories.accessibilityExtraExtraExtraLarge.png +++ b/WireUI/Tests/WireDesignTests/Resources/ReferenceImages/UITabBarController+applyMainTabBarControllerAppearanceTests/testUIFontContentSizeCategories.accessibilityExtraExtraExtraLarge.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48964f814668fd99a4e02e0727233467cb6c33e69f9fb93288da7e32244f47de -size 78847 +oid sha256:24419a84fccd28a7490f33fd037710a08aebfe69d661885ef0405dca1542d37f +size 77816 diff --git a/WireUI/Tests/WireDesignTests/Resources/ReferenceImages/UITabBarController+applyMainTabBarControllerAppearanceTests/testUIFontContentSizeCategories.small.png b/WireUI/Tests/WireDesignTests/Resources/ReferenceImages/UITabBarController+applyMainTabBarControllerAppearanceTests/testUIFontContentSizeCategories.small.png index 6f21914e8d2..dcb69baf11b 100644 --- a/WireUI/Tests/WireDesignTests/Resources/ReferenceImages/UITabBarController+applyMainTabBarControllerAppearanceTests/testUIFontContentSizeCategories.small.png +++ b/WireUI/Tests/WireDesignTests/Resources/ReferenceImages/UITabBarController+applyMainTabBarControllerAppearanceTests/testUIFontContentSizeCategories.small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48964f814668fd99a4e02e0727233467cb6c33e69f9fb93288da7e32244f47de -size 78847 +oid sha256:24419a84fccd28a7490f33fd037710a08aebfe69d661885ef0405dca1542d37f +size 77816 diff --git a/WireUI/Tests/WireDesignTests/Resources/ReferenceImages/UITabBarController+applyMainTabBarControllerAppearanceTests/testUIFontDarkUserInterfaceStyle.1.png b/WireUI/Tests/WireDesignTests/Resources/ReferenceImages/UITabBarController+applyMainTabBarControllerAppearanceTests/testUIFontDarkUserInterfaceStyle.1.png index bfa504ddd0f..3585b133275 100644 --- a/WireUI/Tests/WireDesignTests/Resources/ReferenceImages/UITabBarController+applyMainTabBarControllerAppearanceTests/testUIFontDarkUserInterfaceStyle.1.png +++ b/WireUI/Tests/WireDesignTests/Resources/ReferenceImages/UITabBarController+applyMainTabBarControllerAppearanceTests/testUIFontDarkUserInterfaceStyle.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:356db205f4c9ee65284aca61826a79f8e5df2bf37f109fbdd0fac8e534e0d698 -size 83218 +oid sha256:e9ddd1180b08d95ea5f5103860f6769e1775a27945567cce39a6eebe4211de39 +size 83304 diff --git a/wire-ios-utilities/Source/DeveloperFlag.swift b/wire-ios-utilities/Source/DeveloperFlag.swift index 98ed1b2e8c0..1f492574231 100644 --- a/wire-ios-utilities/Source/DeveloperFlag.swift +++ b/wire-ios-utilities/Source/DeveloperFlag.swift @@ -40,6 +40,7 @@ public enum DeveloperFlag: String, CaseIterable { case wireMeetings case lowKeyPackageCount case enabledCCDebugLogs + case shakeToReport public var description: String { switch self { @@ -96,6 +97,9 @@ public enum DeveloperFlag: String, CaseIterable { case .enabledCCDebugLogs: "Turn on to enable Core Crypto debug logs" + + case .shakeToReport: + "Turn on to enable default shake gesture to present debug report share sheet. Shake again to present DeveloperTools once debug report share sheet presented" } } diff --git a/wire-ios/Tests/Sourcery/generated/AutoMockable.generated.swift b/wire-ios/Tests/Sourcery/generated/AutoMockable.generated.swift index 26911008cd3..3ed281b0eb6 100644 --- a/wire-ios/Tests/Sourcery/generated/AutoMockable.generated.swift +++ b/wire-ios/Tests/Sourcery/generated/AutoMockable.generated.swift @@ -475,6 +475,60 @@ class MockConversationUserClientDetailsActions: ConversationUserClientDetailsAct } +class MockCreateDebugReportUseCaseProtocol: CreateDebugReportUseCaseProtocol { + + // MARK: - Life cycle + + + + // MARK: - invoke + + var invoke_Invocations: [Void] = [] + var invoke_MockError: Error? + var invoke_MockMethod: (() async throws -> URL)? + var invoke_MockValue: URL? + + func invoke() async throws -> URL { + invoke_Invocations.append(()) + + if let error = invoke_MockError { + throw error + } + + if let mock = invoke_MockMethod { + return try await mock() + } else if let mock = invoke_MockValue { + return mock + } else { + fatalError("no mock for `invoke`") + } + } + + // MARK: - invokeData + + var invokeData_Invocations: [Void] = [] + var invokeData_MockError: Error? + var invokeData_MockMethod: (() async throws -> Data)? + var invokeData_MockValue: Data? + + func invokeData() async throws -> Data { + invokeData_Invocations.append(()) + + if let error = invokeData_MockError { + throw error + } + + if let mock = invokeData_MockMethod { + return try await mock() + } else if let mock = invokeData_MockValue { + return mock + } else { + fatalError("no mock for `invokeData`") + } + } + +} + class MockCreateGroupConversationViewControllerBuilderProtocol: CreateGroupConversationViewControllerBuilderProtocol { // MARK: - Life cycle @@ -1444,98 +1498,6 @@ class MockSelfProfileViewControllerBuilderProtocol: SelfProfileViewControllerBui } -class MockSettingsDebugReportRouterProtocol: SettingsDebugReportRouterProtocol { - - // MARK: - Life cycle - - - - // MARK: - presentMailComposer - - var presentMailComposer_Invocations: [Void] = [] - var presentMailComposer_MockMethod: (() -> Void)? - - @MainActor - func presentMailComposer() { - presentMailComposer_Invocations.append(()) - - guard let mock = presentMailComposer_MockMethod else { - fatalError("no mock for `presentMailComposer`") - } - - mock() - } - - // MARK: - presentFallbackAlert - - var presentFallbackAlertSender_Invocations: [UIView] = [] - var presentFallbackAlertSender_MockMethod: ((UIView) -> Void)? - - func presentFallbackAlert(sender: UIView) { - presentFallbackAlertSender_Invocations.append(sender) - - guard let mock = presentFallbackAlertSender_MockMethod else { - fatalError("no mock for `presentFallbackAlertSender`") - } - - mock(sender) - } - - // MARK: - presentShareViewController - - var presentShareViewControllerDestinationsDebugReport_Invocations: [(destinations: [ZMConversation], debugReport: ShareableDebugReport)] = [] - var presentShareViewControllerDestinationsDebugReport_MockMethod: (([ZMConversation], ShareableDebugReport) -> Void)? - - func presentShareViewController(destinations: [ZMConversation], debugReport: ShareableDebugReport) { - presentShareViewControllerDestinationsDebugReport_Invocations.append((destinations: destinations, debugReport: debugReport)) - - guard let mock = presentShareViewControllerDestinationsDebugReport_MockMethod else { - fatalError("no mock for `presentShareViewControllerDestinationsDebugReport`") - } - - mock(destinations, debugReport) - } - -} - -class MockSettingsDebugReportViewModelProtocol: SettingsDebugReportViewModelProtocol { - - // MARK: - Life cycle - - - - // MARK: - sendReport - - var sendReportSender_Invocations: [UIView] = [] - var sendReportSender_MockMethod: ((UIView) -> Void)? - - func sendReport(sender: UIView) { - sendReportSender_Invocations.append(sender) - - guard let mock = sendReportSender_MockMethod else { - fatalError("no mock for `sendReportSender`") - } - - mock(sender) - } - - // MARK: - shareReport - - var shareReport_Invocations: [Void] = [] - var shareReport_MockMethod: (() async -> Void)? - - func shareReport() async { - shareReport_Invocations.append(()) - - guard let mock = shareReport_MockMethod else { - fatalError("no mock for `shareReport`") - } - - await mock() - } - -} - class MockShouldPresentNotificationPermissionHintUseCaseProtocol: ShouldPresentNotificationPermissionHintUseCaseProtocol { // MARK: - Life cycle diff --git a/wire-ios/Tests/Sourcery/generated/AutoMockable.manual.swift b/wire-ios/Tests/Sourcery/generated/AutoMockable.manual.swift index e74456c9ee2..44327e0edae 100644 --- a/wire-ios/Tests/Sourcery/generated/AutoMockable.manual.swift +++ b/wire-ios/Tests/Sourcery/generated/AutoMockable.manual.swift @@ -26,6 +26,7 @@ import CoreLocation import WireDataModel +import WireLogging import WireSyncEngine import WireAccountImageUI import WireMessagingDomain @@ -52,6 +53,94 @@ class MockGetUserByIdUseCaseProtocol: GetUserByIDUseCaseProtocol { } +class MockLogFilesProviding: LogFilesProviding { + + // MARK: - Life cycle + + // MARK: - logFileURLs + + var logFileURLs: [URL] = [] + + // MARK: - info + + var infoSelfUserID_Invocations: [UUID?] = [] + var infoSelfUserID_MockMethod: ((UUID?) -> String)? + var infoSelfUserID_MockValue: String? + + func info(selfUserID: UUID?) -> String { + infoSelfUserID_Invocations.append(selfUserID) + + if let mock = infoSelfUserID_MockMethod { + return mock(selfUserID) + } else if let mock = infoSelfUserID_MockValue { + return mock + } else { + fatalError("no mock for `infoSelfUserID`") + } + } + + // MARK: - clearLogsDirectory + + var clearLogsDirectoryFileManager_Invocations: [FileManager] = [] + var clearLogsDirectoryFileManager_MockError: Error? + var clearLogsDirectoryFileManager_MockMethod: ((FileManager) throws -> Void)? + + func clearLogsDirectory(fileManager: FileManager) throws { + clearLogsDirectoryFileManager_Invocations.append(fileManager) + + if let error = clearLogsDirectoryFileManager_MockError { + throw error + } + + guard let mock = clearLogsDirectoryFileManager_MockMethod else { + return + } + + try mock(fileManager) + } + + // MARK: - removeLogFiles + + var removeLogFilesFileManager_Invocations: [FileManager] = [] + var removeLogFilesFileManager_MockError: Error? + var removeLogFilesFileManager_MockMethod: ((FileManager) throws -> Void)? + + func removeLogFiles(fileManager: FileManager) throws { + removeLogFilesFileManager_Invocations.append(fileManager) + + if let error = removeLogFilesFileManager_MockError { + throw error + } + + guard let mock = removeLogFilesFileManager_MockMethod else { + return + } + + try mock(fileManager) + } + + // MARK: - removeLegacyLogArchives + + var removeLegacyLogArchives_Invocations: [Void] = [] + var removeLegacyLogArchives_MockError: Error? + var removeLegacyLogArchives_MockMethod: (() throws -> Void)? + + func removeLegacyLogArchives() throws { + removeLegacyLogArchives_Invocations.append(()) + + if let error = removeLegacyLogArchives_MockError { + throw error + } + + guard let mock = removeLegacyLogArchives_MockMethod else { + return + } + + try mock() + } + +} + // swiftlint:enable variable_name // swiftlint:enable line_length // swiftlint:enable vertical_whitespace diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForEverythingArchived.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForEverythingArchived.1.png index bbc2f9a5d42..8cc41e1c404 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForEverythingArchived.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForEverythingArchived.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e31776c0bb751ca047d9f7a0c7a32d7d3303374b0d0f0fbeb224a1818b008e29 -size 304669 +oid sha256:80b9365b77a79d70452b435ded72b9aad38a0f30757c205419bd25c320fd4643 +size 304142 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForNoConversations.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForNoConversations.1.png index bbc2f9a5d42..8cc41e1c404 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForNoConversations.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForNoConversations.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e31776c0bb751ca047d9f7a0c7a32d7d3303374b0d0f0fbeb224a1818b008e29 -size 304669 +oid sha256:80b9365b77a79d70452b435ded72b9aad38a0f30757c205419bd25c320fd4643 +size 304142 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByDrafts.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByDrafts.1.png index 5ab79db6f48..3b5f473fee4 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByDrafts.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByDrafts.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f01de9ac2465b158f45032fe208842195eb9492506e0c8e36280c16458087a4c -size 277410 +oid sha256:c8876af270d94f0a808d45785a6792d1cd29df5aeeb3fb8b9e0fd281b9ae408e +size 276841 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByFavourites.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByFavourites.1.png index c28f8acaa40..dcb88e08294 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByFavourites.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByFavourites.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b46a0fabd4930ec241179997ae07606d07a8013b51580708cdd7f2ff138df612 -size 257487 +oid sha256:99263a0ba72ce1574cca37abc76facd397007f4e3c880a3dfdcbb8452bfc3010 +size 256918 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByGroups.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByGroups.1.png index 23ddfcee4a5..f215cdeffc4 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByGroups.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByGroups.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7f85feb398c1c289f4f5e0de0ab7287719a1158d647ca3b144f51636613a494 -size 273546 +oid sha256:af923d43fa5587ad44befe4c85aac43576d913b7c0b4b62a843c6247b580e969 +size 272977 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByMentions.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByMentions.1.png index 8151e6c3bb4..9c867295ff5 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByMentions.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByMentions.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99e37490fe6a4b3774e67d1a6775caa8d0706f932cfad008d04879b0a6f3c632 -size 270241 +oid sha256:66371903364ec893058895c8058bf56b1f75dd81e8e7fa729dff10ab47d37103 +size 269672 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByOneOnOne.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByOneOnOne.1.png index a62a7ed6f37..08240ee7597 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByOneOnOne.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByOneOnOne.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f838ba446a651b7f18258e4af0ab348fc3546ee581c41ff8a67a750daa72789 -size 259513 +oid sha256:d9a869772874578305bca5917053d90a616e2a8d487435f92ea8bb3f75f2531b +size 258944 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByReplies.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByReplies.1.png index 650cc810400..f4b4b3a4c9f 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByReplies.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByReplies.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83dcbb2a9dcc144c91dfc695d92b11e8fe8a4e94e8738b55f867530de87a944e -size 270397 +oid sha256:5a38f4e2e3bd95c38260e0ebd4f462fabc6904487ea84a14b47a67df91349bb9 +size 269828 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByUnread.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByUnread.1.png index aa565f16627..92bd587478d 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByUnread.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsFilteredByUnread.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb78bcfe2d1efd2994b2a0305a1963d8065fada9ae054325df809110db1abf70 -size 275266 +oid sha256:0080a9645f8c4f9deefd7aed3fda376d25bd7b0a82b83c69625ac888b7da3d62 +size 274733 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsWithoutAnyFilterApplied.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsWithoutAnyFilterApplied.1.png index f54e9483a39..5d538a408d4 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsWithoutAnyFilterApplied.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingConversationsWithoutAnyFilterApplied.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb0c06279b4191c199f581cf7c67aa49937bbbe21b2d108eb3e6ae50b6bb51cb -size 316190 +oid sha256:aded62cf27485e302a922ae4f278b0eadf6b0a701052dd8b6284bed4b87074b3 +size 315572 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingFilesTabWhenWireDriveEnabled.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingFilesTabWhenWireDriveEnabled.1.png index a2487a46033..bd5d5e36376 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingFilesTabWhenWireDriveEnabled.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingFilesTabWhenWireDriveEnabled.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1922054c9afd975d797c2de146ae36598f5666a173752220dd26203d3b73310b -size 307976 +oid sha256:e6a6f3fb46dec01b397bc617c897c5eae1538d315c2faa44c694bd94d3d0d8c8 +size 307368 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByDrafts.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByDrafts.1.png index 6628a749819..fdd17351da1 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByDrafts.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByDrafts.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebc02dcb5cf8f1ce55f293da66636df81daa139f6907a74389fb0e3c3afc3431 -size 271722 +oid sha256:ecabee4172e15881b238d511b939e91857a1f83098ad3d7ea2fdc2fe0fbd96b2 +size 271192 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByFavourites.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByFavourites.1.png index 076e2dd7ff7..9f19ff76d49 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByFavourites.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByFavourites.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:073e53212a99d5d70e5afaba2cb379e39ed749807e89bd88d42f9c36953b694f -size 285836 +oid sha256:64cef74cf2bae5ff71e842f6737a0d1873ecfce8c708c49e316f37f55bc99e56 +size 285276 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByGroups.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByGroups.1.png index c2e039dc55f..263f1ccfa3b 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByGroups.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByGroups.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:896a06a3c7f9ce8515112afc66ffdc9de58800c9b65564e4bfb3d5b7bd8b0532 -size 295420 +oid sha256:f6b3f6c6ba641ac295ae41ab23f3d0dbadaa7b6f1e169d67c53f43ec4b7a62bf +size 294901 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByMentions.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByMentions.1.png index edf2d2a94a5..60c91fce2cf 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByMentions.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByMentions.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9269f6e84defdffaf9f6ac285623b7791dfd6e37fac1c59a4d348743aed67326 -size 272295 +oid sha256:52791efae3a5af1cccc86e7b5c13fc3c56a269dc74eef3069826d4d50070d73b +size 271766 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByOneOnOne.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByOneOnOne.1.png index c5db6b450f4..958cf52092a 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByOneOnOne.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByOneOnOne.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c91d5e82b355ee3ed4acd928590711e885369a0977fefb2bd75dc43f1e9a142 -size 295953 +oid sha256:d8b89127931cfc5461fc54b9dd63b2824f876bcdcf66f75b483b3ca6fd76a473 +size 295419 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByReplies.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByReplies.1.png index 75331db8e54..117bf3161c0 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByReplies.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByReplies.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aaf81820d41b8494ece97c86002fe6fb3196b17c6b0b53aba344c8a61d6c0632 -size 271418 +oid sha256:460c42b005d3f775606fb7c9d3cd21b889d0814413b4c794628e8adc4a1592c2 +size 270890 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredBySearchTerm.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredBySearchTerm.1.png index d9273ea4bf0..a2af872383c 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredBySearchTerm.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredBySearchTerm.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0820e699397fb5212c2bc602b1f0be86a2d3ea853a45f9237e2880b8093105b1 -size 279985 +oid sha256:392dbb3aedd4d1da26fd0fabda38aaacaf0baa72eb1052191c2158bed80a42e3 +size 279537 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByUnread.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByUnread.1.png index aa565f16627..92bd587478d 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByUnread.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationListViewControllerSnapshotTests/testForShowingNoConversationsFilteredByUnread.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb78bcfe2d1efd2994b2a0305a1963d8065fada9ae054325df809110db1abf70 -size 275266 +oid sha256:0080a9645f8c4f9deefd7aed3fda376d25bd7b0a82b83c69625ac888b7da3d62 +size 274733 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/SettingsTableViewControllerSnapshotTests/testForAdvancedGroup.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/SettingsTableViewControllerSnapshotTests/testForAdvancedGroup.1.png index c4f28a21af2..7d8060bd0d4 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/SettingsTableViewControllerSnapshotTests/testForAdvancedGroup.1.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/SettingsTableViewControllerSnapshotTests/testForAdvancedGroup.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3201c4f3c5e750eb2225f3ef3a9df7ec301eb6d1ad3cb5c7469c2633332a4187 -size 139911 +oid sha256:f48e395af5105430f012e73b6dc456915dfbdfb1d0b7384f7622533f3d89135b +size 111936 diff --git a/wire-ios/Wire-iOS Tests/Settings/Debug Report/CreateDebugReportUseCaseTests.swift b/wire-ios/Wire-iOS Tests/Settings/Debug Report/CreateDebugReportUseCaseTests.swift new file mode 100644 index 00000000000..bbce2a56908 --- /dev/null +++ b/wire-ios/Wire-iOS Tests/Settings/Debug Report/CreateDebugReportUseCaseTests.swift @@ -0,0 +1,151 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import XCTest + +@testable import Wire + +final class CreateDebugReportUseCaseTests: XCTestCase { + + // MARK: - Properties + + private var sut: CreateDebugReportUseCase! + private var mockLogsProvider: MockLogFilesProviding! + private var tempDirectory: URL! + + // MARK: - setUp / tearDown + + override func setUp() async throws { + tempDirectory = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true) + mockLogsProvider = MockLogFilesProviding() + mockLogsProvider.infoSelfUserID_MockValue = "mock info" + } + + override func tearDown() async throws { + sut = nil + mockLogsProvider = nil + try? FileManager.default.removeItem(at: tempDirectory) + } + + // MARK: - Tests + + func test_invoke_throwsNoLogsError_whenNoLogFilesExist() async { + // GIVEN + mockLogsProvider.logFileURLs = [] + sut = CreateDebugReportUseCase(logsProvider: mockLogsProvider, selfUserID: nil) + + // WHEN / THEN + do { + _ = try await sut.invoke() + XCTFail("Expected noLogs error to be thrown") + } catch CreateDebugReportUseCase.UseCaseError.noLogs { + // Expected + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func test_invoke_returnsZipURL_whenLogsExist() async throws { + // GIVEN + let logFile = try makeTempLogFile(named: "app.log", content: "log content") + mockLogsProvider.logFileURLs = [logFile] + sut = CreateDebugReportUseCase(logsProvider: mockLogsProvider, selfUserID: nil) + + // WHEN + let result = try await sut.invoke() + + // THEN + XCTAssertEqual(result.pathExtension, "zip") + } + + func test_invoke_createsZipFileOnDisk() async throws { + // GIVEN + let logFile = try makeTempLogFile(named: "app.log", content: "log content") + mockLogsProvider.logFileURLs = [logFile] + sut = CreateDebugReportUseCase(logsProvider: mockLogsProvider, selfUserID: nil) + + // WHEN + let result = try await sut.invoke() + + // THEN + XCTAssertTrue(FileManager.default.fileExists(atPath: result.path)) + let attributes = try FileManager.default.attributesOfItem(atPath: result.path) + let fileSize = attributes[.size] as? Int ?? 0 + XCTAssertGreaterThan(fileSize, 0) + } + + func test_invoke_passesCorrectSelfUserIDToInfoProvider() async throws { + // GIVEN + let userID = UUID() + let logFile = try makeTempLogFile(named: "app.log", content: "content") + mockLogsProvider.logFileURLs = [logFile] + sut = CreateDebugReportUseCase(logsProvider: mockLogsProvider, selfUserID: userID) + + // WHEN + _ = try await sut.invoke() + + // THEN + XCTAssertEqual(mockLogsProvider.infoSelfUserID_Invocations.count, 1) + XCTAssertEqual(mockLogsProvider.infoSelfUserID_Invocations[0], userID) + } + + func test_invoke_passesNilSelfUserIDToInfoProvider_whenNoUserID() async throws { + // GIVEN + let logFile = try makeTempLogFile(named: "app.log", content: "content") + mockLogsProvider.logFileURLs = [logFile] + sut = CreateDebugReportUseCase(logsProvider: mockLogsProvider, selfUserID: nil) + + // WHEN + _ = try await sut.invoke() + + // THEN + XCTAssertEqual(mockLogsProvider.infoSelfUserID_Invocations.count, 1) + XCTAssertNil(mockLogsProvider.infoSelfUserID_Invocations[0]) + } + + func test_invoke_handlesMultipleLogFiles() async throws { + // GIVEN + let logFile1 = try makeTempLogFile(named: "app1.log", content: "first log") + let logFile2 = try makeTempLogFile(named: "app2.log", content: "second log") + mockLogsProvider.logFileURLs = [logFile1, logFile2] + sut = CreateDebugReportUseCase(logsProvider: mockLogsProvider, selfUserID: nil) + + // WHEN + let result = try await sut.invoke() + + // THEN + XCTAssertTrue(FileManager.default.fileExists(atPath: result.path)) + + // Filenames are stored as plaintext in the ZIP local file header and central directory, + // so they're searchable in the raw archive bytes even though file contents are deflated. + let zipData = try Data(contentsOf: result) + XCTAssertNotNil(zipData.range(of: Data("app1.log".utf8)), "Expected app1.log entry in zip") + XCTAssertNotNil(zipData.range(of: Data("app2.log".utf8)), "Expected app2.log entry in zip") + } + + // MARK: - Helpers + + private func makeTempLogFile(named name: String, content: String) throws -> URL { + let url = tempDirectory.appendingPathComponent(name) + try content.write(to: url, atomically: true, encoding: .utf8) + return url + } +} diff --git a/wire-ios/Wire-iOS Tests/Settings/Debug Report/LogFilesProviderTests.swift b/wire-ios/Wire-iOS Tests/Settings/Debug Report/LogFilesProviderTests.swift index 2c80cdcd8cd..7c3e8e62ce0 100644 --- a/wire-ios/Wire-iOS Tests/Settings/Debug Report/LogFilesProviderTests.swift +++ b/wire-ios/Wire-iOS Tests/Settings/Debug Report/LogFilesProviderTests.swift @@ -18,6 +18,7 @@ import Foundation import XCTest + @testable import Wire final class LogFilesProviderTests: XCTestCase { @@ -29,31 +30,6 @@ final class LogFilesProviderTests: XCTestCase { provider = LogFilesProvider() } - func test_generateLogFilesZip_createsZipFile() throws { - // GIVEN / WHEN - let zipURL = try provider.generateLogFilesZip() - - // THEN - XCTAssertTrue(FileManager.default.fileExists(atPath: zipURL.path)) - } - - func test_logsDirectoryIsCleanedBeforeArchiving() throws { - // GIVEN - let testDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("logs") - try FileManager.default.createDirectory(at: testDirectory, withIntermediateDirectories: true) - let dummyFile = testDirectory.appendingPathComponent("old.txt") - FileManager.default.createFile(atPath: dummyFile.path, contents: Data("dummy".utf8)) - - XCTAssertTrue(FileManager.default.fileExists(atPath: dummyFile.path)) - - // WHEN - _ = try provider.generateLogFilesZip() - - // THEN - let contents = try FileManager.default.contentsOfDirectory(at: testDirectory, includingPropertiesForKeys: nil) - XCTAssertFalse(contents.contains(where: { $0.lastPathComponent == "old.txt" })) - } - func test_removeLegacyLogArchives_removesArchiveDirectories() throws { // GIVEN let tempDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) @@ -91,31 +67,4 @@ final class LogFilesProviderTests: XCTestCase { XCTAssertTrue(FileManager.default.fileExists(atPath: unrelatedDir.path)) try? FileManager.default.removeItem(at: unrelatedDir) } - - func test_logsDirectoryExists_shouldNotThrow_whenGeneratingLogsZip() throws { - // GIVEN - let logsDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("logs") - try FileManager.default.createDirectory(at: logsDirectory, withIntermediateDirectories: true) - XCTAssertTrue(FileManager.default.fileExists(atPath: logsDirectory.path)) - - // WHEN - let zipURL = try provider.generateLogFilesZip() - - // THEN - XCTAssertTrue(FileManager.default.fileExists(atPath: zipURL.path)) - try? FileManager.default.removeItem(at: logsDirectory) - } - - func test_logsDirectoryIsMissing_shouldCreateIt() throws { - // GIVEN - let logsDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("logs") - try? FileManager.default.removeItem(at: logsDirectory) - - // WHEN - XCTAssertNoThrow(try provider.generateLogFilesZip()) - - // THEN - XCTAssertTrue(FileManager.default.fileExists(atPath: logsDirectory.path)) - } - } diff --git a/wire-ios/Wire-iOS Tests/Settings/Debug Report/SettingsDebugReportViewModelTests.swift b/wire-ios/Wire-iOS Tests/Settings/Debug Report/SettingsDebugReportViewModelTests.swift deleted file mode 100644 index 97d163a2fd2..00000000000 --- a/wire-ios/Wire-iOS Tests/Settings/Debug Report/SettingsDebugReportViewModelTests.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// Wire -// Copyright (C) 2026 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import WireDataModelSupport -import WireLoggingSupport -import WireSyncEngineSupport -import XCTest - -@testable import Wire - -final class SettingsDebugReportViewModelTests: XCTestCase { - - // MARK: - Properties - - private var sut: SettingsDebugReportViewModel! - private var mockRouter: MockSettingsDebugReportRouterProtocol! - private var mockShareFile: MockShareFileUseCaseProtocol! - private var mockFetchShareableConversations: MockFetchShareableConversationsUseCaseProtocol! - private var mockLogsProvider: LogFilesProvidingMock! - private var mockFileMetaDataGenerator: MockFileMetaDataGeneratorProtocol! - - private var coreDataStackHelper: CoreDataStackHelper! - private var coreDataStack: CoreDataStack! - - // MARK: - setUp - - override func setUp() async throws { - - mockRouter = MockSettingsDebugReportRouterProtocol() - mockShareFile = MockShareFileUseCaseProtocol() - mockFetchShareableConversations = MockFetchShareableConversationsUseCaseProtocol() - mockLogsProvider = LogFilesProvidingMock() - mockFileMetaDataGenerator = .init() - - sut = SettingsDebugReportViewModel( - router: mockRouter, - shareFile: mockShareFile, - fetchShareableConversations: mockFetchShareableConversations, - logsProvider: mockLogsProvider, - fileMetaDataGenerator: mockFileMetaDataGenerator - ) - - coreDataStackHelper = CoreDataStackHelper() - coreDataStack = try await coreDataStackHelper.createStack() - } - - // MARK: - tearDown - - override func tearDown() { - sut = nil - mockRouter = nil - mockShareFile = nil - mockFetchShareableConversations = nil - mockLogsProvider = nil - mockFileMetaDataGenerator = nil - coreDataStack = nil - coreDataStackHelper = nil - } - - // MARK: - Tests - - func testShareReport() async { - - // GIVEN - let conversation = await coreDataStack.viewContext.perform { [self] in - ZMConversation.insertNewObject(in: coreDataStack.viewContext) - } - let mockURL = URL(fileURLWithPath: "mockURL") - let mockMetadata = await coreDataStack.viewContext.perform { - ZMFileMetadata(fileURL: mockURL) - } - let mockDebugReport = ShareableDebugReport( - logFileMetadata: mockMetadata, - shareFile: mockShareFile - ) - - // Set mock methods - mockFetchShareableConversations.invoke_MockValue = [conversation] - mockLogsProvider.generateLogFilesZipUrlReturnValue = mockURL - mockFileMetaDataGenerator.metadataForFileAt_MockMethod = { url in - XCTAssertEqual(url, mockURL) - return mockMetadata - } - mockRouter.presentShareViewControllerDestinationsDebugReport_MockMethod = { destinations, debugReport in - XCTAssertEqual(destinations.count, 1) - XCTAssertEqual(destinations.first, conversation) - XCTAssertEqual(debugReport, mockDebugReport) - } - - // WHEN - await sut.shareReport() - - // THEN - XCTAssertEqual(mockFetchShareableConversations.invoke_Invocations.count, 1) - XCTAssertEqual(mockLogsProvider.generateLogFilesZipUrlCallsCount, 1) - XCTAssertEqual(mockFileMetaDataGenerator.metadataForFileAt_Invocations.count, 1) - XCTAssertEqual(mockRouter.presentShareViewControllerDestinationsDebugReport_Invocations.count, 1) - } -} diff --git a/wire-ios/Wire-iOS Tests/Settings/Debug Report/SettingsDebugReportViewControllerSnapshotTests.swift b/wire-ios/Wire-iOS Tests/Settings/Debug Report/ShareDebugBannerViewSnapshotTests.swift similarity index 69% rename from wire-ios/Wire-iOS Tests/Settings/Debug Report/SettingsDebugReportViewControllerSnapshotTests.swift rename to wire-ios/Wire-iOS Tests/Settings/Debug Report/ShareDebugBannerViewSnapshotTests.swift index 51d0abad295..58abdab15c2 100644 --- a/wire-ios/Wire-iOS Tests/Settings/Debug Report/SettingsDebugReportViewControllerSnapshotTests.swift +++ b/wire-ios/Wire-iOS Tests/Settings/Debug Report/ShareDebugBannerViewSnapshotTests.swift @@ -16,25 +16,28 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import SwiftUI import WireTestingPackage import XCTest @testable import Wire -final class SettingsDebugReportViewControllerSnapshotTests: XCTestCase { +final class ShareDebugBannerViewSnapshotTests: XCTestCase { // MARK: - Properties private var snapshotHelper: SnapshotHelper! - private var sut: SettingsDebugReportViewController! + private var sut: UIHostingController! // MARK: - setUp @MainActor override func setUp() async throws { snapshotHelper = SnapshotHelper() - accentColor = .blue - sut = SettingsDebugReportViewController(viewModel: MockSettingsDebugReportViewModelProtocol()) + let view = ShareDebugBannerView(onTap: { /* not relevant for snapshot */ }) + .padding() + sut = UIHostingController(rootView: AnyView(view)) + sut.view.frame = CGRect(x: 0, y: 0, width: 375, height: 120) } // MARK: - tearDown @@ -42,13 +45,18 @@ final class SettingsDebugReportViewControllerSnapshotTests: XCTestCase { override func tearDown() { snapshotHelper = nil sut = nil - super.tearDown() } - // MARK: - Snapshot Test + // MARK: - Tests + + func testLightMode() { + snapshotHelper + .withUserInterfaceStyle(.light) + .verify(matching: sut) + } - func testForInitState() { + func testDarkMode() { snapshotHelper .withUserInterfaceStyle(.dark) .verify(matching: sut) diff --git a/wire-ios/Wire-iOS Tests/Settings/Debug Report/ShareDebugReportViewModelTests.swift b/wire-ios/Wire-iOS Tests/Settings/Debug Report/ShareDebugReportViewModelTests.swift new file mode 100644 index 00000000000..9fe527d9713 --- /dev/null +++ b/wire-ios/Wire-iOS Tests/Settings/Debug Report/ShareDebugReportViewModelTests.swift @@ -0,0 +1,76 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import XCTest + +@testable import Wire + +@MainActor +final class ShareDebugReportViewModelTests: XCTestCase { + + // MARK: - Properties + + private var sut: ShareDebugReportViewModel! + private var mockCreateReport: MockCreateDebugReportUseCaseProtocol! + + // MARK: - setUp + + override func setUp() async throws { + mockCreateReport = MockCreateDebugReportUseCaseProtocol() + sut = ShareDebugReportViewModel( + userSession: nil, + mainCoordinator: nil, + createReport: mockCreateReport + ) + } + + // MARK: - tearDown + + override func tearDown() { + sut = nil + mockCreateReport = nil + } + + // MARK: - Tests + + func testShareViaActivitySheet_invokesCreateReport() async { + // GIVEN + mockCreateReport.invokeReturnValue = URL(fileURLWithPath: "/tmp/logs.zip") + + // WHEN + await sut.shareViaActivitySheet() + + // THEN + XCTAssertEqual(mockCreateReport.invokeCallsCount, 1) + } +} + +// MARK: - Mocks + +final class MockCreateDebugReportUseCaseProtocol: CreateDebugReportUseCaseProtocol { + + var invokeCallsCount = 0 + var invokeReturnValue: URL! + var invokeThrowableError: Error? + + func invoke() async throws -> URL { + invokeCallsCount += 1 + if let error = invokeThrowableError { throw error } + return invokeReturnValue + } +} diff --git a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj index 1c8ac2e699f..41a231cbb22 100644 --- a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj +++ b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj @@ -788,8 +788,6 @@ SecurityLevel/SecurityLevelViewTests.swift, SelfProfileViewControllerTests.swift, "Settings/Debug Report/LogFilesProviderTests.swift", - "Settings/Debug Report/SettingsDebugReportViewControllerSnapshotTests.swift", - "Settings/Debug Report/SettingsDebugReportViewModelTests.swift", Settings/SettingsTests.swift, "Settings+Reset.swift", SettingsTableViewControllerSnapshotTests.swift, @@ -861,6 +859,7 @@ isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( DeviceManagement/MockContextProvider.swift, + "Settings/Debug Report/CreateDebugReportUseCaseTests.swift", Settings/SettingsPropertyTests.swift, ); target = E6579E352AFF9B30004E7FD8 /* Wire-iOS UnitTests */; diff --git a/wire-ios/Wire-iOS/Generated/Strings+Generated.swift b/wire-ios/Wire-iOS/Generated/Strings+Generated.swift index 6d3562dc302..da141f08085 100644 --- a/wire-ios/Wire-iOS/Generated/Strings+Generated.swift +++ b/wire-ios/Wire-iOS/Generated/Strings+Generated.swift @@ -1070,6 +1070,10 @@ internal enum L10n { return L10n.tr("Accessibility", "settings.deviceCount.hint", String(describing: p1), fallback: "%@ devices in use") } } + internal enum ShareDebugInfoBanner { + /// Double tap to open more options + internal static let arrow = L10n.tr("Accessibility", "settings.shareDebugInfoBanner.arrow", fallback: "Double tap to open more options") + } } internal enum ShareProfile { internal enum BackButton { @@ -6042,6 +6046,28 @@ internal enum L10n { internal static let title = L10n.tr("Localizable", "self.settings.receiveNews_and_offers.description.title", fallback: "Receive news and product updates from Wire via email.") } } + internal enum ShareDebugReport { + /// Creating debug report... + internal static let creatingReport = L10n.tr("Localizable", "self.settings.share_debug_report.creating_report", fallback: "Creating debug report...") + internal enum ActionSheet { + /// You can send your report to Wire support, share it with your administrator directly via Wire or any other app. + internal static let message = L10n.tr("Localizable", "self.settings.share_debug_report.action_sheet.message", fallback: "You can send your report to Wire support, share it with your administrator directly via Wire or any other app.") + /// Send email to Support + internal static let sendEmail = L10n.tr("Localizable", "self.settings.share_debug_report.action_sheet.send_email", fallback: "Send email to Support") + /// Share + internal static let share = L10n.tr("Localizable", "self.settings.share_debug_report.action_sheet.share", fallback: "Share") + /// Share via Wire + internal static let shareViaWire = L10n.tr("Localizable", "self.settings.share_debug_report.action_sheet.share_via_wire", fallback: "Share via Wire") + /// Having trouble? + internal static let title = L10n.tr("Localizable", "self.settings.share_debug_report.action_sheet.title", fallback: "Having trouble?") + } + internal enum Banner { + /// To improve Wire's quality, please send us your feedback and let us know about any problems. + internal static let message = L10n.tr("Localizable", "self.settings.share_debug_report.banner.message", fallback: "To improve Wire's quality, please send us your feedback and let us know about any problems.") + /// Having trouble? + internal static let title = L10n.tr("Localizable", "self.settings.share_debug_report.banner.title", fallback: "Having trouble?") + } + } internal enum SoundMenu { /// Sound Alerts internal static let title = L10n.tr("Localizable", "self.settings.sound_menu.title", fallback: "Sound Alerts") @@ -6111,14 +6137,14 @@ internal enum L10n { /// Share Report Via Wire internal static let shareReport = L10n.tr("Localizable", "self.settings.technical_report.share_report", fallback: "Share Report Via Wire") internal enum Mail { - /// Wire Debug Report - internal static let subject = L10n.tr("Localizable", "self.settings.technical_report.mail.subject", fallback: "Wire Debug Report") + /// Issue report + internal static let subject = L10n.tr("Localizable", "self.settings.technical_report.mail.subject", fallback: "Issue report") } internal enum MailBody { - /// Please fill in the following - internal static let firstline = L10n.tr("Localizable", "self.settings.technical_report.mail_body.firstline", fallback: "Please fill in the following") - /// Date and Time of the issue occured: - internal static let section1 = L10n.tr("Localizable", "self.settings.technical_report.mail_body.section1", fallback: "Date and Time of the issue occured:") + /// Please describe what happened below, so that we can understand and reproduce the problem: + internal static let firstline = L10n.tr("Localizable", "self.settings.technical_report.mail_body.firstline", fallback: "Please describe what happened below, so that we can understand and reproduce the problem:") + /// Date and Time of the issue occurred: + internal static let section1 = L10n.tr("Localizable", "self.settings.technical_report.mail_body.section1", fallback: "Date and Time of the issue occurred:") /// What Happened: internal static let section2 = L10n.tr("Localizable", "self.settings.technical_report.mail_body.section2", fallback: "What Happened:") /// Steps to reproduce (if relevant): diff --git a/wire-ios/Wire-iOS/Resources/Localization/Base.lproj/Accessibility.strings b/wire-ios/Wire-iOS/Resources/Localization/Base.lproj/Accessibility.strings index e8d5fdaad64..e8b9090798f 100644 --- a/wire-ios/Wire-iOS/Resources/Localization/Base.lproj/Accessibility.strings +++ b/wire-ios/Wire-iOS/Resources/Localization/Base.lproj/Accessibility.strings @@ -152,6 +152,7 @@ "settings.deviceCount.hint" = "%@ devices in use"; "settings.closeButton.description" = "Close settings"; "settings.backButton.description" = "Go back to Settings"; +"settings.shareDebugInfoBanner.arrow" = "Double tap to open more options"; "accountSettings.backButton.description" = "Go back to Account"; diff --git a/wire-ios/Wire-iOS/Resources/Localization/Base.lproj/Localizable.strings b/wire-ios/Wire-iOS/Resources/Localization/Base.lproj/Localizable.strings index 8ed00a800aa..5c825461acd 100644 --- a/wire-ios/Wire-iOS/Resources/Localization/Base.lproj/Localizable.strings +++ b/wire-ios/Wire-iOS/Resources/Localization/Base.lproj/Localizable.strings @@ -1435,16 +1435,27 @@ "self.settings.technical_report_section.title" = "Debug Report"; "self.settings.technical_report.send_report" = "Send Report"; "self.settings.technical_report.share_report" = "Share Report Via Wire"; -"self.settings.technical_report.mail.subject" = "Wire Debug Report"; +"self.settings.technical_report.mail.subject" = "Issue report"; "self.settings.technical_report.include_log" = "Include detailed log"; "self.settings.technical_report.info" = "If you encounter unexpected behaviour or a bug while using Wire, you can send a bug report to our support team from here. The bug report could contain personal information. \n\nYou can also share your debug logs directly with your team admin via Wire."; "self.settings.technical_report.privacy_warning" = "Detailed logs could contain personal data"; "self.settings.technical_report.no_mail_alert" = "No mail client detected. Tap \"OK\" and send logs manually to: "; -"self.settings.technical_report.mail_body.firstline" = "Please fill in the following"; -"self.settings.technical_report.mail_body.section1" = "Date and Time of the issue occured:"; +"self.settings.technical_report.mail_body.firstline" = "Please describe what happened below, so that we can understand and reproduce the problem:"; +"self.settings.technical_report.mail_body.section1" = "Date and Time of the issue occurred:"; "self.settings.technical_report.mail_body.section2" = "What Happened:"; "self.settings.technical_report.mail_body.section3" = "Steps to reproduce (if relevant):"; + +// Share Debug Report Banner +"self.settings.share_debug_report.banner.title" = "Having trouble?"; +"self.settings.share_debug_report.banner.message" = "To improve Wire's quality, please send us your feedback and let us know about any problems."; +"self.settings.share_debug_report.creating_report" = "Creating debug report..."; +"self.settings.share_debug_report.action_sheet.title" = "Having trouble?"; +"self.settings.share_debug_report.action_sheet.message" = "You can send your report to Wire support, share it with your administrator directly via Wire or any other app."; +"self.settings.share_debug_report.action_sheet.share_via_wire" = "Share via Wire"; +"self.settings.share_debug_report.action_sheet.send_email" = "Send email to Support"; +"self.settings.share_debug_report.action_sheet.share" = "Share"; + // Password reset "self.settings.password_reset_menu.title" = "Reset Password"; diff --git a/wire-ios/Wire-iOS/Sources/Components/ShareViewController/ShareViewController+Views.swift b/wire-ios/Wire-iOS/Sources/Components/ShareViewController/ShareViewController+Views.swift index 571b4b3475a..2eecb4b919a 100644 --- a/wire-ios/Wire-iOS/Sources/Components/ShareViewController/ShareViewController+Views.swift +++ b/wire-ios/Wire-iOS/Sources/Components/ShareViewController/ShareViewController+Views.swift @@ -18,6 +18,7 @@ import UIKit import WireDesign +import WireLocators extension ShareViewController { @@ -75,6 +76,7 @@ extension ShareViewController { destinationsTableView.dataSource = self closeButton.accessibilityLabel = "close" + closeButton.accessibilityIdentifier = Locators.ShareViaWirePage.closeButton.rawValue closeButton.setIcon(.cross, size: .tiny, for: .normal) closeButton.setIconColor(SemanticColors.Icon.foregroundDefault, for: .normal) closeButton.addTarget( @@ -86,6 +88,7 @@ extension ShareViewController { let sendButtonIconColor = SemanticColors.Icon.foregroundDefaultWhite sendButton.accessibilityLabel = "send" + sendButton.accessibilityIdentifier = Locators.ShareViaWirePage.sendButton.rawValue sendButton.isEnabled = false sendButton.setIcon(.send, size: .tiny, for: .normal) sendButton.setBackgroundImageColor(UIColor.accent(), for: .normal) diff --git a/wire-ios/Wire-iOS/Sources/Developer/DebugAlert.swift b/wire-ios/Wire-iOS/Sources/Developer/DebugAlert.swift index 566e18e4c17..55d28c28126 100644 --- a/wire-ios/Wire-iOS/Sources/Developer/DebugAlert.swift +++ b/wire-ios/Wire-iOS/Sources/Developer/DebugAlert.swift @@ -113,29 +113,29 @@ enum DebugAlert { popoverPresentationConfiguration: PopoverPresentationControllerConfiguration ) -> UIAlertAction { UIAlertAction(title: L10n.Localizable.General.ok, style: .default) { _ in - let logFilesProvider = LogFilesProvider() - let logsFileURL: URL - do { - logsFileURL = try logFilesProvider.generateLogFilesZip() - } catch { - WireLogger.system.error("Failed to generate log files zip: \(error)") - return - } - - let activityViewController = UIActivityViewController( - activityItems: [logsFileURL], - applicationActivities: nil - ) - activityViewController.configurePopoverPresentationController(using: popoverPresentationConfiguration) - activityViewController.completionWithItemsHandler = { _, _, _, _ in + Task { @MainActor in + let useCase = CreateDebugReportUseCase(selfUserID: nil) + let logsFileURL: URL do { - try logFilesProvider.clearLogsDirectory(fileManager: .default) + logsFileURL = try await useCase.invoke() } catch { - WireLogger.system.warn("Unable to clear temporary directory: \(error)") + WireLogger.system.error("Failed to generate log files zip: \(error)") + return } - } - controller.present(activityViewController, animated: true, completion: nil) + let activityViewController = UIActivityViewController( + activityItems: [logsFileURL], + applicationActivities: nil + ) + activityViewController.configurePopoverPresentationController(using: popoverPresentationConfiguration) + activityViewController.completionWithItemsHandler = { _, _, _, _ in + Task { + try? LogFilesProvider().clearLogsDirectory(fileManager: .default) + } + } + + controller.present(activityViewController, animated: true, completion: nil) + } } } } @@ -175,7 +175,7 @@ final class DebugLogSender: NSObject, MFMailComposeViewControllerDelegate { let mailVC = MFMailComposeViewController() mailVC.setToRecipients([mail]) mailVC.setSubject("iOS logs from \(userDescription)") - let body = mailVC.prefilledBody(withMessage: message) + let body = MFMailComposeViewController.prefilledBody(withMessage: message) mailVC.setMessageBody(body, isHTML: false) mailVC.mailComposeDelegate = alert @@ -184,8 +184,14 @@ final class DebugLogSender: NSObject, MFMailComposeViewControllerDelegate { senderInstance = alert Task { - await mailVC.attachLogs() - // as UIViewController is marked @MainActor, this will be executed on mainThread automatically + do { + let zipURL = try await CreateDebugReportUseCase(selfUserID: nil).invoke() + if let data = try? Data(contentsOf: zipURL) { + mailVC.addAttachmentData(data, mimeType: "application/zip", fileName: "logs.zip") + } + } catch { + WireLogger.system.debug("no logs to attach: \(error)") + } await presentingViewController.present(mailVC, animated: true, completion: nil) } } diff --git a/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DebugActions/DeveloperDebugActionsViewModel.swift b/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DebugActions/DeveloperDebugActionsViewModel.swift index 8e2781eead4..f39e92558e8 100644 --- a/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DebugActions/DeveloperDebugActionsViewModel.swift +++ b/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DebugActions/DeveloperDebugActionsViewModel.swift @@ -55,6 +55,7 @@ final class DeveloperDebugActionsViewModel: ObservableObject { private let userSession: ZMUserSession? private let selfClient: UserClient? private let onDismiss: (() -> Void)? + private let shareDebugPresenter = ShareDebugReportPresenter() private let logger = WireLogger(tag: "developer") @@ -74,7 +75,7 @@ final class DeveloperDebugActionsViewModel: ObservableObject { private func setupButtons() { let buttonItems: [DeveloperDebugActionsDisplayModel.ButtonItem] = [ - .init(title: "Send debug logs", action: sendDebugLogs), + .init(title: "Share debug logs", action: shareDebugLogs), .init(title: "Trigger incremental sync", action: triggerIncrementalSync), .init(title: "Trigger resources sync", action: triggerResourcesSync), .init(title: "Break next incremental sync", action: breakNextIncrementalSync), @@ -291,30 +292,11 @@ final class DeveloperDebugActionsViewModel: ObservableObject { onDismiss?() } - // MARK: Send Logs + // MARK: Share Logs - private func sendDebugLogs() { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, - let rootViewController = appDelegate.mainWindow?.rootViewController else { - return - } - - var presentingViewController = rootViewController - while let presentedViewController = presentingViewController.presentedViewController { - presentingViewController = presentedViewController - } - - DebugLogSender.sendLogsByEmail( - message: "Send logs", - presentingViewController: presentingViewController, - fallbackActivityPopoverConfiguration: .sourceView( - sourceView: presentingViewController.view, - sourceRect: .init( - origin: presentingViewController.view.safeAreaLayoutGuide.layoutFrame.origin, - size: .zero - ) - ) - ) + @MainActor + private func shareDebugLogs() { + shareDebugPresenter.present(from: UIApplication.shared.topmostViewController(onlyFullScreen: false)) } // MARK: Quick Sync diff --git a/wire-ios/Wire-iOS/Sources/LaunchSequenceOperation.swift b/wire-ios/Wire-iOS/Sources/LaunchSequenceOperation.swift index b74d70a2204..19f5e142269 100644 --- a/wire-ios/Wire-iOS/Sources/LaunchSequenceOperation.swift +++ b/wire-ios/Wire-iOS/Sources/LaunchSequenceOperation.swift @@ -20,6 +20,7 @@ import avs import Foundation import WireCommonComponents import WireDesign +import WireFoundation import WireLogging import WireSyncEngine @@ -45,6 +46,12 @@ final class DeveloperFlagOperation: LaunchSequenceOperation { let isOn = keyAndValue[1] == "true" flag.enable(isOn) } + #if DEBUG + for (key, value) in UITestConfig.environment.developerFlags { + guard let flag = DeveloperFlag(rawValue: key) else { continue } + flag.enable(value) + } + #endif } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Blacklist/BlockerViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Blacklist/BlockerViewController.swift index 1ec76d9e7e5..55c5b761b4f 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Blacklist/BlockerViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Blacklist/BlockerViewController.swift @@ -16,7 +16,6 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import MessageUI import UIKit import WireLocators import WireLogging @@ -38,6 +37,7 @@ final class BlockerViewController: LaunchImageViewController { private var context: BlockerViewControllerContext = .blacklist private var error: Error? private var sessionManager: SessionManager? + private let shareDebugPresenter = ShareDebugReportPresenter() private var observerTokens = [Any]() @@ -131,20 +131,8 @@ final class BlockerViewController: LaunchImageViewController { title: Strings.sendLogs, style: .default ) { [weak self] _ in - guard let self else { - return - } - DebugLogSender.sendLogsByEmail( - message: debugLogMessage, - presentingViewController: self, - fallbackActivityPopoverConfiguration: .sourceView( - sourceView: view, - sourceRect: .init( - origin: view.safeAreaLayoutGuide.layoutFrame.origin, - size: .zero - ) - ) - ) + guard let self else { return } + shareDebugPresenter.present(from: self) } ) @@ -264,11 +252,7 @@ final class BlockerViewController: LaunchImageViewController { style: .default ) { [weak self] _ in guard let self else { return } - let fallbackActivityPopoverConfiguration = PopoverPresentationControllerConfiguration.sourceView( - sourceView: view, - sourceRect: .init(origin: view.safeAreaLayoutGuide.layoutFrame.origin, size: .zero) - ) - presentMailComposer(fallbackActivityPopoverConfiguration: fallbackActivityPopoverConfiguration) + shareDebugPresenter.present(from: self) } databaseFailureAlert.addAction(reportError) @@ -325,17 +309,6 @@ final class BlockerViewController: LaunchImageViewController { present(deleteDatabaseConfirmationAlert, animated: true) } - func mailComposeController( - _ controller: MFMailComposeViewController, - didFinishWith result: MFMailComposeResult, - error: Error? - ) { - // shown after sending report logs, we should show other choices again - // in order not to be stuck on black screen - controller.presentingViewController?.dismiss(animated: true) { - self.showDatabaseFailureMessage() - } - } } // MARK: - Application state observing @@ -350,8 +323,6 @@ extension BlockerViewController: ApplicationStateObserving { } } -extension BlockerViewController: SendTechnicalReportPresenter {} - // MARK: - Certificate enrollment extension BlockerViewController { diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+Advanced.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+Advanced.swift index 6714d1e7041..ddffe251744 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+Advanced.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+Advanced.swift @@ -32,9 +32,8 @@ extension SettingsCellDescriptorFactory { mainCoordinator: any MainCoordinatorProtocol ) -> any SettingsCellDescriptorType { let items = [ - troubleshootingSection(userSession: userSession, mainCoordinator: mainCoordinator), - debuggingToolsSection, - pushSection + pushSection, + debuggingToolsSection ] return SettingsGroupCellDescriptor( @@ -50,38 +49,6 @@ extension SettingsCellDescriptorFactory { // MARK: - Sections - private func troubleshootingSection( - userSession: UserSession, - mainCoordinator: any MainCoordinatorProtocol - ) -> SettingsSectionDescriptor { - let submitDebugButton = SettingsExternalScreenCellDescriptor( - title: SelfSettingsAdvancedLocale.Troubleshooting.SubmitDebug.title, - presentationAction: { () -> (UIViewController?) in - let router = SettingsDebugReportRouter(userSession: userSession, mainCoordinator: mainCoordinator) - let shareFile = ShareFileUseCase(contextProvider: userSession.contextProvider) - let fetchShareableConversations = FetchShareableConversationsUseCase( - contextProvider: userSession - .contextProvider - ) - let viewModel = SettingsDebugReportViewModel( - router: router, - shareFile: shareFile, - fetchShareableConversations: fetchShareableConversations, - fileMetaDataGenerator: FileMetaDataGenerator() - ) - let viewController = SettingsDebugReportViewController(viewModel: viewModel) - router.viewController = viewController - return viewController - } - ) - - return SettingsSectionDescriptor( - cellDescriptors: [submitDebugButton], - header: SelfSettingsAdvancedLocale.Troubleshooting.title, - footer: SelfSettingsAdvancedLocale.Troubleshooting.SubmitDebug.subtitle - ) - } - private var pushSection: SettingsSectionDescriptor { let pushButton = SettingsExternalScreenCellDescriptor( title: SelfSettingsAdvancedLocale.ResetPushToken.title, @@ -95,7 +62,7 @@ extension SettingsCellDescriptorFactory { return SettingsSectionDescriptor( cellDescriptors: [pushButton], - header: .none, + header: SelfSettingsAdvancedLocale.Troubleshooting.title, footer: SelfSettingsAdvancedLocale.ResetPushToken.subtitle, visibilityAction: { _ in true diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/CreateDebugReportUseCase.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/CreateDebugReportUseCase.swift new file mode 100644 index 00000000000..fa9a9a43ba6 --- /dev/null +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/CreateDebugReportUseCase.swift @@ -0,0 +1,100 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import WireLogging +import ZIPFoundation + +// sourcery: AutoMockable +protocol CreateDebugReportUseCaseProtocol { + func invoke() async throws -> URL + func invokeData() async throws -> Data +} + +final class CreateDebugReportUseCase: CreateDebugReportUseCaseProtocol { + + enum UseCaseError: Error { + case noLogs(description: String) + } + + private let logsProvider: LogFilesProviding + private let selfUserID: UUID? + + init(logsProvider: LogFilesProviding = LogFilesProvider(), selfUserID: UUID?) { + self.logsProvider = logsProvider + self.selfUserID = selfUserID + } + + func invoke() async throws -> URL { + try await withCheckedThrowingContinuation { [logsProvider, selfUserID] continuation in + DispatchQueue.global(qos: .userInitiated).async { + do { + continuation.resume(returning: try Self.createZip( + logsProvider: logsProvider, + selfUserID: selfUserID + )) + } catch { + continuation.resume(throwing: error) + } + } + } + } + + func invokeData() async throws -> Data { + let url = try await invoke() + return try await Task.detached(priority: .userInitiated) { + defer { try? FileManager.default.removeItem(at: url) } + return try Data(contentsOf: url) + }.value + } + + // MARK: - Private + + private static func createZip(logsProvider: LogFilesProviding, selfUserID: UUID?) throws -> URL { + let fileManager = FileManager.default + let logsDirectory = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("logs", isDirectory: true) + + try? fileManager.removeItem(at: logsDirectory) + + let logFileURLs = logsProvider.logFileURLs + guard !logFileURLs.isEmpty else { + throw UseCaseError.noLogs(description: logFileURLs.description) + } + + try fileManager.createDirectory(at: logsDirectory, withIntermediateDirectories: true) + + let archiveFolder = logsDirectory.appendingPathComponent(UUID().uuidString) + try fileManager.createDirectory(at: archiveFolder, withIntermediateDirectories: true) + + let info = logsProvider.info(selfUserID: selfUserID) + let infoFileURL = archiveFolder.appendingPathComponent("info.txt") + try info.write(to: infoFileURL, atomically: true, encoding: .utf8) + + for url in logFileURLs { + let copy = archiveFolder.appending(path: url.lastPathComponent, directoryHint: .notDirectory) + try fileManager.copyItem(at: url, to: copy) + } + + let zipURL = logsDirectory.appendingPathComponent("logs.zip") + try fileManager.zipItem(at: archiveFolder, to: zipURL, shouldKeepParent: false, compressionMethod: .deflate) + try fileManager.removeItem(at: archiveFolder) + + return zipURL + } +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/LogFilesProvider.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/LogFilesProvider.swift index a10b7aa100a..99c24ea34bd 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/LogFilesProvider.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/LogFilesProvider.swift @@ -20,41 +20,22 @@ import UIKit import WireCommonComponents import WireDomain import WireLogging -import WireSyncEngine import WireSystem -import ZIPFoundation -/// Generates log files archives. -/// -/// All logs are stored at the `NSTemporaryDirectory` URL (`tmp`) in the folder `//logs/`. -/// -/// When generating the logs archive, we create a unique directory for the archive in `//logs//logs.zip`. -/// -/// The logs folder `//logs/` is cleared: -/// - after `generateLogFilesData()` returns -/// - when calling `generateLogFilesZip()`, before the archive is created -/// - when calling `clearLogsDirectory()` -/// -/// In each logs archive, an extra file `info.txt` is added. It contains general information about the app. +/// Provides raw log file URLs and device/app info for building debug archives. /// +/// Archiving (ZIP creation) is handled by `CreateDebugReportUseCase`. struct LogFilesProvider: LogFilesProviding { - // MARK: - Types - - enum Error: Swift.Error { - case noLogs(description: String) - } - // MARK: - Properties private let logsDirectory = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) .appendingPathComponent("logs", isDirectory: true) - private var logFilesURLs: [URL] { + var logFileURLs: [URL] { let fileManager = FileManager.default var urls = ZMSLog.pathsForExistingLogs - // add the root directory of the app, NSE and SE logs if let appGroupIdentifier = Bundle.main.applicationGroupIdentifier, let sharedLogsDirectoryURL = fileManager.sharedLogsDirectoryURL(for: appGroupIdentifier) { let targetLogDirectories = try? fileManager.contentsOfDirectory( @@ -67,118 +48,42 @@ struct LogFilesProvider: LogFilesProviding { return urls } - // MARK: - Interface - - func generateLogFilesData() throws -> Data { - let fileManager = FileManager.default - defer { - try? clearLogsDirectory(fileManager: fileManager) - } - - let logFilesURL = try generateLogFilesZip() - return try Data(contentsOf: logFilesURL) - } - - func generateLogFilesZip() throws -> URL { - let fileManager = FileManager.default - try? clearLogsDirectory(fileManager: fileManager) - - // Determine files to export - let logFilesURLs = logFilesURLs - guard !logFilesURLs.isEmpty else { - throw Error.noLogs(description: logFilesURLs.description) - } - - // Re-create the base directory - try fileManager.createDirectory(at: logsDirectory, withIntermediateDirectories: true) - - // Create a subfolder for the current session - let archiveFolder = logsDirectory.appendingPathComponent(UUID().uuidString) - try fileManager.createDirectory(at: archiveFolder, withIntermediateDirectories: true) - - // Create the info file - _ = try createInfoFile(at: archiveFolder) - - // Copy files to be zipped - for logFilesURL in logFilesURLs { - let copy = archiveFolder.appending(path: logFilesURL.lastPathComponent, directoryHint: .notDirectory) - try fileManager.copyItem(at: logFilesURL, to: copy) - } - - // Create the zip file - let zipURL = logsDirectory.appendingPathComponent("logs.zip") - try fileManager.zipItem(at: archiveFolder, to: zipURL, shouldKeepParent: false, compressionMethod: .deflate) - - // Clean up - try fileManager.removeItem(at: archiveFolder) - - return zipURL - } - - func clearLogsDirectory(fileManager: FileManager) throws { - if fileManager.fileExists(atPath: logsDirectory.path) { - try fileManager.removeItem(at: logsDirectory) - } - } - - func removeLogFiles(fileManager: FileManager) throws { - for fileURL in logFilesURLs { - try fileManager.removeItem(at: fileURL) - } - } - - // MARK: - Helpers - - func info(includingJournal: Bool = false) -> String { - let date = Date() + // MARK: - LogFilesProviding + func info(selfUserID: UUID?) -> String { var body = """ App Version: \(Bundle.main.appInfo.fullVersion) Bundle id: \(Bundle.main.bundleIdentifier ?? "-") Device: \(UIDevice.current.zm_model()) iOS version: \(UIDevice.current.systemVersion) - Date: \(date.transportString()) + Date: \(Date().transportString()) """ - if includingJournal { - body += "\n\nJournal:\n\(journalInfos())" + if let selfUserID { + let journal = Journal(userID: selfUserID, storage: UserDefaults.shared()) + let entries = journal.values().compactMap { "\($0): \($1)" }.joined(separator: "\n") + body += "\n\nJournal:\n\(entries)" } if let datadogUserIdentifier = WireAnalytics.Datadog.userIdentifier { - // display only when enabled body.append("\nDatadog ID: \(datadogUserIdentifier)") } + return body } - private func journalInfos() -> String { - guard let selfUserID = ZMUserSession.shared()?.selfUser.remoteIdentifier else { - return "Not Available" + func clearLogsDirectory(fileManager: FileManager) throws { + if fileManager.fileExists(atPath: logsDirectory.path) { + try fileManager.removeItem(at: logsDirectory) } - - let journal = Journal( - userID: selfUserID, - storage: UserDefaults.shared() - ) - - return journal.values().compactMap { "\($0): \($1)" }.joined(separator: "\n") } - private func createInfoFile(at url: URL) throws -> URL { - let infoFileURL = url.appendingPathComponent("info.txt") - - let info = self.info(includingJournal: true) - try info.write( - to: infoFileURL, - atomically: true, - encoding: .utf8 - ) - - return infoFileURL + func removeLogFiles(fileManager: FileManager) throws { + for fileURL in logFileURLs { + try fileManager.removeItem(at: fileURL) + } } - /// Deletes all log-related archives and folders created in the temp directory. - /// This includes any leftover directories that match the pattern used in `logsDirectory`. func removeLegacyLogArchives() throws { let tempDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let fileManager = FileManager.default @@ -186,7 +91,6 @@ struct LogFilesProvider: LogFilesProviding { let contents = try fileManager.contentsOfDirectory(at: tempDir, includingPropertiesForKeys: nil) for url in contents { - // The `logsDirectory` structure is /tmp//logs let logsSubdir = url.appendingPathComponent("logs") var isDirectory: ObjCBool = false @@ -196,5 +100,4 @@ struct LogFilesProvider: LogFilesProviding { } } } - } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/MailComposeViewModifier.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/MailComposeViewModifier.swift new file mode 100644 index 00000000000..e46cb748d0f --- /dev/null +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/MailComposeViewModifier.swift @@ -0,0 +1,27 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation + +struct MailComposeItem: Identifiable { + let id = UUID() + let recipient: String + let subject: String + let messageBody: String + let attachmentData: Data +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportRouter.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportRouter.swift deleted file mode 100644 index 0a6126902b0..00000000000 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportRouter.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// Wire -// Copyright (C) 2026 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import MessageUI -import WireDataModel -import WireMainNavigationUI -import WireReusableUIComponents -import WireSyncEngine - -// sourcery: AutoMockable -protocol SettingsDebugReportRouterProtocol { - - /// Presents the mail composer with the debug report - - @MainActor - func presentMailComposer() - - /// Presents the fallback alert - - func presentFallbackAlert(sender: UIView) - - /// Presents the share view controller - /// - /// - Parameters: - /// - destinations: list of conversations to choose from to send the report - /// - debugReport: the debug report to share - - func presentShareViewController( - destinations: [ZMConversation], - debugReport: ShareableDebugReport - ) -} - -final class SettingsDebugReportRouter: NSObject, SettingsDebugReportRouterProtocol { - - // MARK: - Properties - - weak var viewController: UIViewController? - - private let mailRecipient = WireEmail.shared.supportEmail - private let userSession: UserSession - private let mainCoordinator: any MainCoordinatorProtocol - - init(userSession: UserSession, mainCoordinator: any MainCoordinatorProtocol) { - self.userSession = userSession - self.mainCoordinator = mainCoordinator - } - - private lazy var activityIndicator = { - let topMostViewController = UIApplication.shared.topmostViewController(onlyFullScreen: false) - return BlockingActivityIndicator(view: topMostViewController!.view) - }() - - // MARK: - Interface - - func presentShareViewController( - destinations: [ZMConversation], - debugReport: ShareableDebugReport - ) { - - let shareViewController = ShareViewController( - shareable: debugReport, - destinations: destinations, - showPreview: true, - userSession: userSession, - mainCoordinator: mainCoordinator - ) - - shareViewController.onDismiss = { shareController, _ in - shareController.dismiss(animated: true) - } - - viewController?.present(shareViewController, animated: true) - } - - @MainActor - func presentMailComposer() { - let mailComposeViewController = MFMailComposeViewController() - mailComposeViewController.mailComposeDelegate = self - mailComposeViewController.setToRecipients([mailRecipient]) - mailComposeViewController.setSubject(L10n.Localizable.Self.Settings.TechnicalReport.Mail.subject) - let body = mailComposeViewController.prefilledBody() - mailComposeViewController.setMessageBody(body, isHTML: false) - - activityIndicator.start() - Task.detached(priority: .userInitiated) { [activityIndicator] in - await mailComposeViewController.attachLogs() - - await self.viewController?.present(mailComposeViewController, animated: true, completion: nil) - await MainActor.run { - activityIndicator.stop() - } - } - } - - @MainActor - func presentFallbackAlert(sender: UIView) { - guard let viewController else { return } - - DebugAlert.displayFallbackActivityController( - email: mailRecipient, - from: viewController, - popoverPresentationConfiguration: .superviewAndFrame(of: sender, insetBy: (dx: -4, dy: -4)) - ) - } -} - -extension SettingsDebugReportRouter: MFMailComposeViewControllerDelegate { - - func mailComposeController( - _ controller: MFMailComposeViewController, - didFinishWith result: MFMailComposeResult, - error: Error? - ) { - controller.dismiss(animated: true) - } -} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportViewController.swift deleted file mode 100644 index 34ceeb57c94..00000000000 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportViewController.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// Wire -// Copyright (C) 2026 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import MessageUI -import UIKit -import WireDataModel -import WireDesign - -class SettingsDebugReportViewController: UIViewController { - - // MARK: - Constants - - private enum LayoutConstants { - static let spacing: CGFloat = 8 - static let padding: CGFloat = 20 - static let safeBottomPadding: CGFloat = 30 - static let buttonHeight: CGFloat = 48 - } - - // MARK: - Types - - private typealias Strings = L10n.Localizable.Self.Settings - - // MARK: - Properties - - private let viewModel: SettingsDebugReportViewModelProtocol - - // MARK: - Views - - private lazy var infoLabel: UILabel = { - let label = DynamicFontLabel( - text: Strings.TechnicalReport.info, - style: .body1, - color: SemanticColors.Label.textDefault - ) - label.numberOfLines = 0 - label.textAlignment = .left - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private lazy var sendReportButton = createButton( - title: Strings.TechnicalReport.sendReport.capitalized, - action: UIAction { [weak self] action in - self?.didTapSendReport(sender: action.sender as! UIButton) - } - ) - - private lazy var shareReportButton: UIButton = createButton( - title: Strings.TechnicalReport.shareReport.capitalized, - action: UIAction { [weak self] _ in self?.didTapShareReport() } - ) - - // MARK: - Life cycle - - init(viewModel: SettingsDebugReportViewModelProtocol) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = SemanticColors.View.backgroundDefault - - setupViews() - setupConstraints() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - setupNavigationBarTitle(Strings.TechnicalReportSection.title.capitalized) - } - - // MARK: - Setup - - private func setupViews() { - view.addSubview(infoLabel) - view.addSubview(sendReportButton) - view.addSubview(shareReportButton) - } - - private func setupConstraints() { - NSLayoutConstraint.activate([ - infoLabel.topAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.topAnchor, - constant: LayoutConstants.padding - ), - infoLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: LayoutConstants.padding), - infoLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -LayoutConstants.padding), - - shareReportButton.bottomAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.bottomAnchor, - constant: -LayoutConstants.safeBottomPadding - ), - shareReportButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: LayoutConstants.padding), - shareReportButton.trailingAnchor.constraint( - equalTo: view.trailingAnchor, - constant: -LayoutConstants.padding - ), - shareReportButton.heightAnchor.constraint(greaterThanOrEqualToConstant: LayoutConstants.buttonHeight), - - sendReportButton.bottomAnchor.constraint( - equalTo: shareReportButton.topAnchor, - constant: -LayoutConstants.spacing - ), - sendReportButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: LayoutConstants.padding), - sendReportButton.trailingAnchor.constraint( - equalTo: view.trailingAnchor, - constant: -LayoutConstants.padding - ), - sendReportButton.heightAnchor.constraint(greaterThanOrEqualToConstant: LayoutConstants.buttonHeight) - ]) - } - - // MARK: - Actions - - @objc - private func didTapSendReport(sender: UIView) { - viewModel.sendReport(sender: sender) - } - - @objc - private func didTapShareReport() { - Task { await viewModel.shareReport() } - } - - // MARK: - Helpers - - private func createButton(title: String, action: UIAction) -> UIButton { - let button = ZMButton( - style: .secondaryTextButtonStyle, - cornerRadius: 16, - fontSpec: .buttonBigSemibold - ) - button.setTitle(title, for: .normal) - button.addAction(action, for: .touchUpInside) - button.translatesAutoresizingMaskIntoConstraints = false - return button - } -} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportViewModel.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportViewModel.swift deleted file mode 100644 index 8a73dea0962..00000000000 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportViewModel.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// Wire -// Copyright (C) 2026 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import MessageUI -import WireCommonComponents -import WireLogging -import WireSyncEngine - -final class SettingsDebugReportViewModel: SettingsDebugReportViewModelProtocol { - - // MARK: - Properties - - private let router: SettingsDebugReportRouterProtocol - private let shareFile: ShareFileUseCaseProtocol - private let fetchShareableConversations: FetchShareableConversationsUseCaseProtocol - private let logsProvider: LogFilesProviding - private let fileMetaDataGenerator: FileMetaDataGeneratorProtocol - - // MARK: - Life cycle - - init( - router: SettingsDebugReportRouterProtocol, - shareFile: ShareFileUseCaseProtocol, - fetchShareableConversations: FetchShareableConversationsUseCaseProtocol, - logsProvider: LogFilesProviding = LogFilesProvider(), - fileMetaDataGenerator: FileMetaDataGeneratorProtocol - ) { - self.router = router - self.shareFile = shareFile - self.fetchShareableConversations = fetchShareableConversations - self.logsProvider = logsProvider - self.fileMetaDataGenerator = fileMetaDataGenerator - } - - // MARK: - Interface - - func sendReport(sender: UIView) { - if MFMailComposeViewController.canSendMail() { - Task { - await router.presentMailComposer() - } - } else { - router.presentFallbackAlert(sender: sender) - } - } - - @MainActor - func shareReport() async { - - do { - let conversations = fetchShareableConversations.invoke() - let logsURL = try logsProvider.generateLogFilesZip() - let metadata = await fileMetaDataGenerator.metadataForFile(at: logsURL) - let shareableDebugReport = ShareableDebugReport(logFileMetadata: metadata, shareFile: shareFile) - router.presentShareViewController( - destinations: conversations, - debugReport: shareableDebugReport - ) - } catch { - WireLogger.system.error("failed to generate log files \(error)") - } - } -} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/ShareDebugBannerView.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/ShareDebugBannerView.swift new file mode 100644 index 00000000000..6f27e2a27e9 --- /dev/null +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/ShareDebugBannerView.swift @@ -0,0 +1,81 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireDesign +import WireLocators + +struct ShareDebugBannerView: View { + + let onTap: () -> Void + + var body: some View { + Button { onTap() } label: { + HStack(alignment: .center) { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Image(systemName: "exclamationmark.bubble") + .font(for: .body3) + .accessibilityHidden(true) + .foregroundColor(ColorTheme.Backgrounds.onBackground.color) + + VStack(alignment: .leading, spacing: 4) { + title + message + } + .frame(maxWidth: .infinity, alignment: .leading) + } + Image(systemName: "chevron.right") + .font(for: .h3) + .accessibilityLabel(Text(L10n.Accessibility.Settings.ShareDebugInfoBanner.arrow)) + } + .padding(12) + .background( + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(ColorTheme.Backgrounds.surface.color) + ) + .overlay( + RoundedRectangle(cornerRadius: 8, style: .continuous) + .stroke(Color(.separator)) + ) + } + .buttonStyle(.plain) + .accessibilityLabel(L10n.Localizable.Self.Settings.ShareDebugReport.Banner.title) + .accessibilityIdentifier(Locators.SettingsPage.shareDebugBanner.rawValue) + } + + @ViewBuilder var title: some View { + Text(L10n.Localizable.Self.Settings.ShareDebugReport.Banner.title) + .font(for: .body3) + .multilineTextAlignment(.leading) + .foregroundColor(ColorTheme.Backgrounds.onBackground.color) + } + + @ViewBuilder var message: some View { + Text(L10n.Localizable.Self.Settings.ShareDebugReport.Banner.message) + .font(for: .h4) + .multilineTextAlignment(.leading) + .foregroundColor(ColorTheme.Content.Base.secondary.color) + } + +} + +#Preview { + ShareDebugBannerView(onTap: {}) + .padding() + .background(Color(.systemGroupedBackground)) +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/ShareDebugReportPresenter.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/ShareDebugReportPresenter.swift new file mode 100644 index 00000000000..897c9acb4d6 --- /dev/null +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/ShareDebugReportPresenter.swift @@ -0,0 +1,161 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Combine +import MessageUI +import UIKit +import WireLocators +import WireSyncEngine + +final class ShareDebugReportPresenter: NSObject { + + private(set) var isPresenting = false + private weak var presentedSheet: UIAlertController? + private var mailCancellable: AnyCancellable? + private var mailDelegate: MailComposeDelegate? + + @MainActor + func dismiss(completion: @escaping @MainActor () -> Void) { + guard isPresenting, let sheet = presentedSheet else { + completion() + return + } + sheet.dismiss(animated: true) { [weak self] in + self?.isPresenting = false + self?.presentedSheet = nil + completion() + } + } + + @MainActor + func present(from topMostViewController: UIViewController?) { + guard !isPresenting, let viewController = topMostViewController else { return } + isPresenting = true + + let userSession = SessionManager.shared?.activeUserSession + let mainCoordinator = ZClientViewController.shared?.mainCoordinator + let selfUserID = userSession?.selfUser.remoteIdentifier + + let viewModel = ShareDebugReportViewModel( + userSession: userSession, + mainCoordinator: mainCoordinator, + selfUserID: selfUserID + ) + + typealias l10n = L10n.Localizable.Self.Settings.ShareDebugReport.ActionSheet + typealias ids = Locators.ShareDebugReportPage + let actionSheet = UIAlertController( + title: l10n.title, + message: l10n.message, + preferredStyle: .actionSheet + ) + actionSheet.view.accessibilityIdentifier = ids.actionSheet.rawValue + + if viewModel.canShareViaWire { + actionSheet.addAction(UIAlertAction( + title: l10n.shareViaWire, + style: .default, + accessibilityIdentifier: ids.shareViaWireButton.rawValue + ) { [weak self] _ in + self?.isPresenting = false + Task { await viewModel.shareViaWire() } + }) + } + if viewModel.canSendEmail { + actionSheet.addAction(UIAlertAction( + title: l10n.sendEmail, + style: .default, + accessibilityIdentifier: ids.sendEmailButton.rawValue + ) { [weak self] _ in + self?.isPresenting = false + Task { await viewModel.sendEmail() } + }) + } + actionSheet.addAction(UIAlertAction( + title: l10n.share, + style: .default, + accessibilityIdentifier: ids.shareButton.rawValue + ) { [weak self] _ in + self?.isPresenting = false + Task { await viewModel.shareViaActivitySheet() } + }) + actionSheet.addAction(UIAlertAction( + title: L10n.Localizable.General.cancel, + style: .cancel, + accessibilityIdentifier: ids.cancelButton.rawValue + ) { [weak self] _ in + self?.isPresenting = false + }) + + if let popover = actionSheet.popoverPresentationController { + popover.sourceView = viewController.view + popover.sourceRect = CGRect( + x: viewController.view.bounds.midX, + y: viewController.view.bounds.midY, + width: 0, + height: 0 + ) + popover.permittedArrowDirections = [] + } + + presentedSheet = actionSheet + viewController.present(actionSheet, animated: true) + + mailCancellable = viewModel.$mailComposeItem + .compactMap(\.self) + .receive(on: RunLoop.main) + .sink { [weak self, weak viewController] item in + guard MFMailComposeViewController.canSendMail() else { return } + let mailVC = MFMailComposeViewController() + let delegate = MailComposeDelegate { [weak self] in + self?.mailDelegate = nil + viewModel.mailComposeItem = nil + } + self?.mailDelegate = delegate + mailVC.mailComposeDelegate = delegate + mailVC.setToRecipients([item.recipient]) + mailVC.setSubject(item.subject) + mailVC.setMessageBody(item.messageBody, isHTML: false) + mailVC.addAttachmentData(item.attachmentData, mimeType: "application/zip", fileName: "logs.zip") + if let popover = mailVC.popoverPresentationController, let view = viewController?.view { + popover.sourceView = view + popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0) + popover.permittedArrowDirections = [] + } + viewController?.present(mailVC, animated: true) + } + } +} + +private final class MailComposeDelegate: NSObject, MFMailComposeViewControllerDelegate { + + private let onDismiss: () -> Void + + init(onDismiss: @escaping () -> Void) { + self.onDismiss = onDismiss + } + + func mailComposeController( + _ controller: MFMailComposeViewController, + didFinishWith result: MFMailComposeResult, + error: Error? + ) { + controller.dismiss(animated: true) + onDismiss() + } +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/ShareDebugReportViewModel.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/ShareDebugReportViewModel.swift new file mode 100644 index 00000000000..4ae17acfd3f --- /dev/null +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/ShareDebugReportViewModel.swift @@ -0,0 +1,154 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Combine +import MessageUI +import UIKit +import WireCommonComponents +import WireDataModel +import WireLogging +import WireMainNavigationUI +import WireReusableUIComponents +import WireSyncEngine + +@MainActor +final class ShareDebugReportViewModel: ObservableObject { + + @Published var mailComposeItem: MailComposeItem? + + let canShareViaWire: Bool + let canSendEmail: Bool + + private let userSession: UserSession? + private let mainCoordinator: (any MainCoordinatorProtocol)? + private let mailRecipient: String + private let createReport: CreateDebugReportUseCaseProtocol + + init( + userSession: UserSession?, + mainCoordinator: (any MainCoordinatorProtocol)?, + selfUserID: UUID? = nil, + mailRecipient: String = WireEmail.shared.supportEmail, + createReport: CreateDebugReportUseCaseProtocol? = nil + ) { + self.userSession = userSession + self.mainCoordinator = mainCoordinator + self.mailRecipient = mailRecipient + self.createReport = createReport ?? CreateDebugReportUseCase(selfUserID: selfUserID) + self.canShareViaWire = userSession != nil && mainCoordinator != nil + self.canSendEmail = MFMailComposeViewController.canSendMail() + } + + func shareViaWire() async { + guard let userSession, let mainCoordinator else { return } + guard let viewController = topViewController() else { return } + await withReport(from: viewController) { url in + let shareFile = ShareFileUseCase(contextProvider: userSession.contextProvider) + let fetchConversations = FetchShareableConversationsUseCase(contextProvider: userSession.contextProvider) + let conversations = fetchConversations.invoke() + let metadata = await FileMetaDataGenerator().metadataForFile(at: url) + let report = ShareableDebugReport(logFileMetadata: metadata, shareFile: shareFile) + let shareVC = ShareViewController( + shareable: report, + destinations: conversations, + showPreview: true, + userSession: userSession, + mainCoordinator: mainCoordinator + ) + shareVC.onDismiss = { vc, _ in vc.dismiss(animated: true) } + if let popover = shareVC.popoverPresentationController { + popover.sourceView = viewController.view + popover.sourceRect = CGRect( + x: viewController.view.bounds.midX, + y: viewController.view.bounds.midY, + width: 0, + height: 0 + ) + popover.permittedArrowDirections = [] + } + viewController.present(shareVC, animated: true) + } + } + + func shareViaActivitySheet() async { + guard let viewController = topViewController() else { return } + await withReport(from: viewController) { url in + let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil) + if let popover = activityVC.popoverPresentationController { + popover.sourceView = viewController.view + popover.sourceRect = CGRect( + x: viewController.view.bounds.midX, + y: viewController.view.bounds.midY, + width: 0, + height: 0 + ) + popover.permittedArrowDirections = [] + } + viewController.present(activityVC, animated: true) + } + } + + func sendEmail() async { + guard let viewController = topViewController() else { return } + let indicator = BlockingActivityIndicator( + view: viewController.view, + accessibilityAnnouncement: nil, + style: .card + ) + indicator.start(text: L10n.Localizable.Self.Settings.ShareDebugReport.creatingReport) + do { + let data = try await createReport.invokeData() + indicator.stop() + mailComposeItem = MailComposeItem( + recipient: mailRecipient, + subject: L10n.Localizable.Self.Settings.TechnicalReport.Mail.subject, + messageBody: MFMailComposeViewController.prefilledBody(), + attachmentData: data + ) + } catch { + indicator.stop() + WireLogger.system.error("failed to create debug report: \(error)") + } + } + + // MARK: - Private + + private func withReport( + from viewController: UIViewController, + then action: @escaping @MainActor (URL) async -> Void + ) async { + let indicator = BlockingActivityIndicator( + view: viewController.view, + accessibilityAnnouncement: nil, + style: .card + ) + indicator.start(text: L10n.Localizable.Self.Settings.ShareDebugReport.creatingReport) + do { + let url = try await createReport.invoke() + indicator.stop() + await action(url) + } catch { + indicator.stop() + WireLogger.system.error("failed to create debug report: \(error)") + } + } + + private func topViewController() -> UIViewController? { + UIApplication.shared.topmostViewController(onlyFullScreen: false) + } +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/MFMailComposeViewController+Logs.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/MFMailComposeViewController+Logs.swift index a7ed644dfec..5d1a2a53102 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/MFMailComposeViewController+Logs.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/MFMailComposeViewController+Logs.swift @@ -19,20 +19,15 @@ import Foundation import MessageUI import WireCommonComponents -import WireLogging import WireSystem extension MFMailComposeViewController { - func prefilledBody(withMessage message: String = "") -> String { - var body = """ - --DO NOT EDIT-- - \(LogFilesProvider().info()) - ---------------\n - """ - + static func prefilledBody(withMessage message: String = "") -> String { + // swiftformat:disable:next redundantStaticSelf typealias l10n = L10n.Localizable.Self.Settings.TechnicalReport.MailBody - let details = """ + + return """ \(l10n.firstline) - \(l10n.section1) @@ -43,18 +38,6 @@ extension MFMailComposeViewController { - \(l10n.section3) - """ - body.append("\n\(details)\n") - return body - } - - func attachLogs() { - do { - let data = try LogFilesProvider().generateLogFilesData() - addAttachmentData(data, mimeType: "application/zip", fileName: "logs.zip") - } catch { - WireLogger.system.debug("no logs for WireLogger to send: \(String(describing: error))") - } } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/SendTechnicalReportPresenter.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/SendTechnicalReportPresenter.swift index c557b49eb4c..3e52f00ead8 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/SendTechnicalReportPresenter.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/SendTechnicalReportPresenter.swift @@ -18,6 +18,8 @@ import MessageUI import UIKit +import WireCommonComponents +import WireLogging import WireReusableUIComponents import WireSystem @@ -46,20 +48,24 @@ extension SendTechnicalReportPresenter where Self: UIViewController { mailComposeViewController.mailComposeDelegate = self mailComposeViewController.setToRecipients([mailRecipient]) mailComposeViewController.setSubject(L10n.Localizable.Self.Settings.TechnicalReport.Mail.subject) - let body = mailComposeViewController.prefilledBody() + let body = MFMailComposeViewController.prefilledBody() mailComposeViewController.setMessageBody(body, isHTML: false) let topMostViewController = UIApplication.shared.topmostViewController(onlyFullScreen: false) let activityIndicator = BlockingActivityIndicator(view: topMostViewController!.view) activityIndicator.start() - Task.detached(priority: .userInitiated) { - await mailComposeViewController.attachLogs() - - await self.present(mailComposeViewController, animated: true, completion: nil) - await MainActor.run { - activityIndicator.stop() + Task { + do { + let zipURL = try await CreateDebugReportUseCase(selfUserID: nil).invoke() + if let data = try? Data(contentsOf: zipURL) { + mailComposeViewController.addAttachmentData(data, mimeType: "application/zip", fileName: "logs.zip") + } + } catch { + WireLogger.system.debug("no logs to attach: \(error)") } + await self.present(mailComposeViewController, animated: true, completion: nil) + activityIndicator.stop() } } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/SettingsTableViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/SettingsTableViewController.swift index 166731dc672..77fe7c519b9 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/SettingsTableViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/SettingsTableViewController.swift @@ -31,6 +31,13 @@ class SettingsBaseTableViewController: UIViewController { let settingsCoordinator: AnySettingsCoordinator + var footerViewController: UIViewController? { + didSet { + guard isViewLoaded else { return } + installFooterViewController() + } + } + fileprivate final class IntrinsicSizeTableView: UITableView { override var contentSize: CGSize { didSet { @@ -73,6 +80,22 @@ class SettingsBaseTableViewController: UIViewController { createTableView() view.addSubview(topSeparator) createConstraints() + installFooterViewController() + } + + private func installFooterViewController() { + guard let footer = footerViewController else { return } + + addChild(footer) + footer.view.translatesAutoresizingMaskIntoConstraints = false + footerContainer.addSubview(footer.view) + NSLayoutConstraint.activate([ + footer.view.topAnchor.constraint(equalTo: footerContainer.topAnchor, constant: 12), + footer.view.leadingAnchor.constraint(equalTo: footerContainer.leadingAnchor, constant: 16), + footer.view.trailingAnchor.constraint(equalTo: footerContainer.trailingAnchor, constant: -16), + footer.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12) + ]) + footer.didMove(toParent: self) } override func viewDidAppear(_ animated: Bool) { @@ -91,6 +114,7 @@ class SettingsBaseTableViewController: UIViewController { tableView.estimatedRowHeight = 56 view.addSubview(tableView) view.addSubview(footerContainer) + footerContainer.backgroundColor = SemanticColors.View.backgroundDefault footerContainer.addSubview(footerSeparator) footerSeparator.inverse = true } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/SettingsViewControllerBuilder.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/SettingsViewControllerBuilder.swift index 3ba2b659660..c5f13bf62cd 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/SettingsViewControllerBuilder.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/SettingsViewControllerBuilder.swift @@ -16,6 +16,7 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import SwiftUI import WireMainNavigationUI import WireSettingsUI import WireSyncEngine @@ -74,11 +75,22 @@ final class SettingsViewControllerBuilder: MainSettingsUIBuilderProtocol, MainSe useTypeIntrinsicSizeTableView: false, mainCoordinator: mainCoordinator ) - return .init( + let vc = SettingsTableViewController( group: group, settingsCoordinator: .init(settingsCoordinator: settingsCoordinator), userSession: userSession ) + vc.footerViewController = makeShareDebugBannerVC(mainCoordinator: mainCoordinator) + return vc + } + + private func makeShareDebugBannerVC(mainCoordinator: some MainCoordinatorProtocol) -> UIViewController { + let presenter = ShareDebugReportPresenter() + let bannerVC = UIHostingController(rootView: ShareDebugBannerView { + presenter.present(from: UIApplication.shared.topmostViewController(onlyFullScreen: false)) + }) + bannerVC.view.backgroundColor = .clear + return bannerVC } func build( diff --git a/wire-ios/Wire-iOS/Sources/WireApplication.swift b/wire-ios/Wire-iOS/Sources/WireApplication.swift index c7aa0e8cfcf..cd3e02f4501 100644 --- a/wire-ios/Wire-iOS/Sources/WireApplication.swift +++ b/wire-ios/Wire-iOS/Sources/WireApplication.swift @@ -17,25 +17,100 @@ // import WireCommonComponents +import WireFoundation import WireSyncEngine final class WireApplication: UIApplication { - private let presenter = DeveloperToolsPresenter() + private let developerToolsPresenter = DeveloperToolsPresenter() + private let shareDebugPresenter = ShareDebugReportPresenter() + private var tripleTapGestureRecognizer: UITapGestureRecognizer? + + override init() { + super.init() + setupTripleTapGestureForSimulator() + } + + deinit { + #if targetEnvironment(simulator) + NotificationCenter.default.removeObserver(self) + #endif + } override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { + guard motion == .motionShake else { return } + handleShakeAction() + } + + @objc + private func handleShakeAction() { guard Bundle.developerModeEnabled else { + presentDebugShareSheet() return } - guard motion == .motionShake else { return } + guard DeveloperFlag.shakeToReport.isOn else { + presentDeveloperTools() + return + } - if let appDelegate = UIApplication.shared.delegate as? AppDelegate { - presenter.presentIfNotDisplayed( - with: appDelegate.appRootRouter, - from: self.topmostViewController(onlyFullScreen: false) - ) + guard shareDebugPresenter.isPresenting else { + presentDebugShareSheet() + return } + + shareDebugPresenter.dismiss { [weak self] in + self?.presentDeveloperTools() + } + } + + private func presentDeveloperTools() { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return + } + developerToolsPresenter.presentIfNotDisplayed( + with: appDelegate.appRootRouter, + from: self.topmostViewController(onlyFullScreen: false) + ) + } + + private func presentDebugShareSheet() { + shareDebugPresenter.present(from: topmostViewController(onlyFullScreen: false)) + } + + // MARK: - UITest support + + // Triple tap gesture for simulator (used in XCUITests) + private func setupTripleTapGestureForSimulator() { + #if targetEnvironment(simulator) + guard Bundle.developerModeEnabled else { + return + } + + NotificationCenter.default.addObserver( + self, + selector: #selector(windowDidBecomeKey), + name: UIWindow.didBecomeKeyNotification, + object: nil + ) + #endif + } + + @objc + private func windowDidBecomeKey(_ notification: Notification) { + #if targetEnvironment(simulator) + guard let window = notification.object as? UIWindow, + window === keyWindow, + tripleTapGestureRecognizer == nil else { + return + } + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleShakeAction)) + tapGesture.numberOfTapsRequired = 3 + tapGesture.cancelsTouchesInView = false + window.addGestureRecognizer(tapGesture) + tripleTapGestureRecognizer = tapGesture + #endif } } diff --git a/wire-ios/WireUITests/Helper/EnvironmentVariables.swift b/wire-ios/WireUITests/Helper/EnvironmentVariables.swift index b57caadb9ad..1c6d0315d11 100644 --- a/wire-ios/WireUITests/Helper/EnvironmentVariables.swift +++ b/wire-ios/WireUITests/Helper/EnvironmentVariables.swift @@ -18,7 +18,7 @@ import Foundation struct EnvironmentVariables { - enum Failure: Error { + enum Failure: LocalizedError { case missingBackendURL case missingInbucketURL case missingInbucketUsername @@ -31,6 +31,23 @@ struct EnvironmentVariables { case missingCallingInstanceTypeName case missingCallingInstanceTypeVersion case missingOktaApiKey + + var errorDescription: String? { + switch self { + case .missingBackendURL: "Missing env var: BACKEND_URL" + case .missingInbucketURL: "Missing env var: INBUCKET_URL / ANTA_INBUCKET_URL / BELLA_INBUCKET_URL" + case .missingInbucketUsername: "Missing env var: INBUCKET_USERNAME" + case .missingInbucketPassword: "Missing env var: INBUCKET_PASSWORD" + case .missingDeepLinkURL: "Missing env var: ANTA_DEEPLINK_URL / BELLA_DEEPLINK_URL" + case .missingCallingServiceURL: "Missing env var: CALLINGSERVICE_URL" + case .missingCallingServiceUsername: "Missing env var: CALLINGSERVICE_USERNAME" + case .missingCallingServicePassword: "Missing env var: CALLINGSERVICE_PASSWORD" + case .missingCallingBackend: "Missing env var: PREDEFINED_BACKEND" + case .missingCallingInstanceTypeName: "Missing env var: CALLING_INSTANCE_TYPE_NAME" + case .missingCallingInstanceTypeVersion: "Missing env var: CALLING_INSTANCE_TYPE_VERSION" + case .missingOktaApiKey: "Missing env var: OKTA_API_KEY_IOS" + } + } } private let stagingBackendURL: URL diff --git a/wire-ios/WireUITests/Helper/WireUITestCase.swift b/wire-ios/WireUITests/Helper/WireUITestCase.swift index d78092c7dff..1ff663d105e 100644 --- a/wire-ios/WireUITests/Helper/WireUITestCase.swift +++ b/wire-ios/WireUITests/Helper/WireUITestCase.swift @@ -39,6 +39,7 @@ class WireUITestCase: XCTestCase { XCUIApplication().terminate() callingServiceClient = try CallingServiceClient() registerNotificationPermissionMonitor() + uiTestConfig.useTripleTapForShakeGesture = true let launchArguments = [ "-resetData", @@ -137,6 +138,10 @@ class WireUITestCase: XCTestCase { return true } } + + func simulateShakeGesture() { + app.tap(withNumberOfTaps: 3, numberOfTouches: 1) + } } extension XCUIApplication { diff --git a/wire-ios/WireUITests/Pages/ActivitySheetPage.swift b/wire-ios/WireUITests/Pages/ActivitySheetPage.swift new file mode 100644 index 00000000000..7469fcc6856 --- /dev/null +++ b/wire-ios/WireUITests/Pages/ActivitySheetPage.swift @@ -0,0 +1,38 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// +import WireLocators +import XCTest + +class ActivitySheetPage: PageModel { + + private typealias ids = Locators.ActivitySheetPage + + override var pageMainElement: XCUIElement { + app.otherElements[ids.sheet.rawValue].firstMatch + } + + var saveToFilesButton: XCUIElement { + app.cells[ids.saveToFiles.rawValue].firstMatch + } + + @discardableResult + func selectSaveToFiles() throws -> OnMyiPhonePage { + saveToFilesButton.waitAndTap() + return try OnMyiPhonePage() + } +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportViewModelProtocol.swift b/wire-ios/WireUITests/Pages/DeveloperToolsPage.swift similarity index 68% rename from wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportViewModelProtocol.swift rename to wire-ios/WireUITests/Pages/DeveloperToolsPage.swift index 073e1091734..66685ae26f7 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportViewModelProtocol.swift +++ b/wire-ios/WireUITests/Pages/DeveloperToolsPage.swift @@ -16,14 +16,16 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import UIKit +import XCTest -// sourcery: AutoMockable -protocol SettingsDebugReportViewModelProtocol { +class DeveloperToolsPage: PageModel { - /// Send a debug report via email or shows fallback alert if email is not available - func sendReport(sender: UIView) + override var pageMainElement: XCUIElement { + app.navigationBars["Developer tools"] + } + + func hide() { + app.buttons["Close"].firstMatch.tap() + } - /// Presents a list of conversation for the user to share the debug report with - func shareReport() async } diff --git a/wire-ios/WireUITests/Pages/OnMyiPhonePage.swift b/wire-ios/WireUITests/Pages/OnMyiPhonePage.swift index 3b779aa41ab..e095a977c66 100644 --- a/wire-ios/WireUITests/Pages/OnMyiPhonePage.swift +++ b/wire-ios/WireUITests/Pages/OnMyiPhonePage.swift @@ -49,6 +49,10 @@ class OnMyiPhonePage: PageModel { return try BackupOrRestorePage() } + func save() { + saveButton.tap() + } + func selectBackupFile(withName name: String) throws -> SetPasswordPage { backupFile(name).tap() return try SetPasswordPage() diff --git a/wire-ios/WireUITests/Pages/SettingsPage.swift b/wire-ios/WireUITests/Pages/SettingsPage.swift index 6c5cdcafcdb..6492e275c4d 100644 --- a/wire-ios/WireUITests/Pages/SettingsPage.swift +++ b/wire-ios/WireUITests/Pages/SettingsPage.swift @@ -43,11 +43,22 @@ class SettingsPage: PageModel { return try AccountSettingsPage() } + var shareDebugBanner: XCUIElement { + app.buttons[Locators.SettingsPage.shareDebugBanner.rawValue].firstMatch + } + func openOptionsMenu() throws -> OptionsOnSettingsPage { optionsMenu.tap() return try OptionsOnSettingsPage() } + @discardableResult + func tapShareDebugBanner() throws -> ShareDebugReportPage { + XCTAssertTrue(shareDebugBanner.waitForExistence(timeout: 5)) + shareDebugBanner.tap() + return try ShareDebugReportPage() + } + func switchToConversationsTab() throws -> ConversationsPage { conversationsTab.tap() return try ConversationsPage() diff --git a/wire-ios/WireUITests/Pages/ShareDebugReportPage.swift b/wire-ios/WireUITests/Pages/ShareDebugReportPage.swift new file mode 100644 index 00000000000..fa4a91b18a0 --- /dev/null +++ b/wire-ios/WireUITests/Pages/ShareDebugReportPage.swift @@ -0,0 +1,62 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// +import WireLocators +import XCTest + +class ShareDebugReportPage: PageModel { + + private typealias ids = Locators.ShareDebugReportPage + + override var pageMainElement: XCUIElement { + app.descendants(matching: .any)[ids.actionSheet.rawValue].firstMatch + } + + var shareViaWireButton: XCUIElement { + app.descendants(matching: .any)[ids.shareViaWireButton.rawValue].firstMatch + } + + var sendEmailButton: XCUIElement { + app.descendants(matching: .any)[ids.sendEmailButton.rawValue].firstMatch + } + + var shareButton: XCUIElement { + app.descendants(matching: .any)[ids.shareButton.rawValue].firstMatch + } + + var cancelButton: XCUIElement { + app.descendants(matching: .any)[ids.cancelButton.rawValue].firstMatch + } + + @discardableResult + func selectShare() -> Self { + shareButton.tap() + return self + } + + @discardableResult + func selectShareViaWire() throws -> ShareViaWirePage { + shareViaWireButton.tap() + return try ShareViaWirePage() + } + + @discardableResult + func selectSendEmail() -> Self { + sendEmailButton.tap() + return self + } +} diff --git a/wire-ios/WireUITests/Pages/ShareViaWirePage.swift b/wire-ios/WireUITests/Pages/ShareViaWirePage.swift new file mode 100644 index 00000000000..425a67d5060 --- /dev/null +++ b/wire-ios/WireUITests/Pages/ShareViaWirePage.swift @@ -0,0 +1,60 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// +import WireLocators +import XCTest + +class ShareViaWirePage: PageModel { + + private typealias ids = Locators.ShareViaWirePage + + override var pageMainElement: XCUIElement { + closeButton + } + + var sendButton: XCUIElement { + app.descendants(matching: .any)[ids.sendButton.rawValue].firstMatch + } + + var closeButton: XCUIElement { + app.descendants(matching: .any)[ids.closeButton.rawValue].firstMatch + } + + func conversationCell(name: String) -> XCUIElement { + app.staticTexts[name].firstMatch + } + + @discardableResult + func selectConversation(name: String) -> Self { + let cell = conversationCell(name: name) + XCTAssertTrue(cell.waitForExistence(timeout: 5), "Conversation '\(name)' not found in share picker") + cell.tap() + return self + } + + @discardableResult + func send() -> Self { + sendButton.waitAndTap() + return self + } + + @discardableResult + func close() -> Self { + closeButton.waitAndTap() + return self + } +} diff --git a/wire-ios/WireUITests/ShareDebugReportTests.swift b/wire-ios/WireUITests/ShareDebugReportTests.swift new file mode 100644 index 00000000000..fbe03f28997 --- /dev/null +++ b/wire-ios/WireUITests/ShareDebugReportTests.swift @@ -0,0 +1,116 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import XCTest + +final class ShareDebugReportTests: WireUITestCase { + + @MainActor + override func setUpWithError() throws { + uiTestConfig[.shakeToReport] = true + try super.setUpWithError() + } + + /// On the login screen there is no user session and no mail client (simulator), + /// so only "Share" and "Cancel" should appear — 2 options total. + func testShakeGesture_onLoginScreen_presentsShareDebugActionSheet_TC_10853() throws { + // GIVEN + _ = try WelcomePage() + + // WHEN + simulateShakeGesture() + let shareDebugPage = try ShareDebugReportPage() + + // THEN + XCTAssertTrue(shareDebugPage.shareButton.exists, "Share button should be present") + XCTAssertTrue(shareDebugPage.cancelButton.exists, "Cancel button should be present") + XCTAssertFalse( + shareDebugPage.shareViaWireButton.exists, + "Share via Wire should not be available without a session" + ) + XCTAssertFalse(shareDebugPage.sendEmailButton.exists, "Send email should not be available on simulator") + + shareDebugPage.selectShare() + + // Save the report to Files via the native share sheet + try ActivitySheetPage() + .selectSaveToFiles() + .save() + } + + /// Once logged in, "Share via Wire" is also available — 3 options total. + /// "Send email to Support" is still absent because the simulator has no mail client. + @MainActor + func testShakeGesture_onConversationScreen_presentsShareDebugActionSheet_TC_10854() async throws { + // GIVEN + let user = try await UserHelper.default.createPersonalUser() + _ = try await loginToBackend(user: user) + + // WHEN + simulateShakeGesture() + let shareDebugPage = try ShareDebugReportPage() + + // THEN + XCTAssertTrue(shareDebugPage.shareViaWireButton.exists, "Share via Wire should be available when logged in") + XCTAssertTrue(shareDebugPage.shareButton.exists, "Share button should be present") + XCTAssertTrue(shareDebugPage.cancelButton.exists, "Cancel button should be present") + XCTAssertFalse(shareDebugPage.sendEmailButton.exists, "Send email should not be available on simulator") + } + + /// The debug report banner in Settings should also trigger the action sheet, + /// and the report can be shared to a group conversation via Wire. + @MainActor + func testShareDebugReportBanner_TC_10855() async throws { + // GIVEN + let groupName = UserGenerator.generateRandomConversationName() + let (_, owner) = try await UserHelper.default.registerUserAsTeamOwner() + let ownerToken = try await UserHelper.default.fetchAccessToken(email: owner.email, password: owner.password) + let teamID = try XCTUnwrap(owner.teamID) + let (memberQualifiedID, _) = try await UserHelper.default.registerUsersAsTeamMember( + ownerAccessToken: ownerToken.token, + teamID: teamID + ) + try await UserHelper.default.createGroupConversations( + qualifiedIds: [memberQualifiedID], + owner: owner, + groupName: groupName + ) + + let conversationsPage = try await loginToBackend(user: owner) + let settingsPage = try conversationsPage.openSettings() + XCTAssertTrue( + settingsPage.shareDebugBanner.waitForExistence(timeout: 10), + "Share debug banner should be visible on Settings page" + ) + + // WHEN + try settingsPage.tapShareDebugBanner() + .selectShareViaWire() + + try ShareViaWirePage() + .selectConversation(name: groupName) + .send() + + // THEN + let activeConversation = try ActiveConversationPage() + XCTAssertTrue( + activeConversation.fileLabels.firstMatch.waitForExistence(timeout: 30), + "File message should appear in '\(groupName)' after sharing the debug report" + ) + } +} diff --git a/wire-ios/WireUITests/XCUIApplication+Additions.swift b/wire-ios/WireUITests/XCUIApplication+Additions.swift index 49fdcafff24..3ea9b9412a8 100644 --- a/wire-ios/WireUITests/XCUIApplication+Additions.swift +++ b/wire-ios/WireUITests/XCUIApplication+Additions.swift @@ -16,9 +16,18 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import WireFoundation import WireUtilities import XCTest +extension UITestConfig { + /// Gets or sets a `DeveloperFlag` value by its typed enum case. + subscript(flag: DeveloperFlag) -> Bool { + get { developerFlags[flag.rawValue] ?? false } + set { developerFlags[flag.rawValue] = newValue } + } +} + extension XCUIApplication { func setDeveloperFlags(_ flags: [DeveloperFlag: Bool]) {