diff --git a/Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m b/Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m index b5997339cd..d165ddff02 100644 --- a/Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m +++ b/Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m @@ -74,9 +74,14 @@ - (BOOL)application:(UIApplication *)application uiForm.submitButtonLabel = @"Report that jank"; uiForm.messagePlaceholder = @"Describe the nature of the jank. Its essence, if you will."; + uiForm.useSentryUser = YES; }; config.configureTheme = ^(SentryUserFeedbackThemeConfiguration *_Nonnull theme) { theme.font = [UIFont fontWithName:@"ChalkboardSE-Regular" size:25]; + theme.outlineStyle = + [[SentryFormElementOutlineStyle alloc] initWithColor:UIColor.purpleColor + cornerRadius:10 + outlineWidth:4]; }; config.onSubmitSuccess = ^(NSDictionary *_Nonnull info) { NSString *name = info[@"name"] ?: @"$shakespearean_insult_name"; diff --git a/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift b/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift index 3aacd36b24..78b4ae64c8 100644 --- a/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift +++ b/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift @@ -123,25 +123,16 @@ extension UserFeedbackUITests { try assertHookMarkersNotExist() widgetButton.tap() - XCTAssert(nameField.waitForExistence(timeout: 1)) try assertOnlyHookMarkersExist(names: [.onFormOpen]) - nameField.tap() let testName = "Andrew" - nameField.typeText(testName) - - emailField.tap() let testEmail = "custom@email.com" - emailField.typeText(testEmail) - - messageTextView.tap() let testMessage = "UITest user feedback" - messageTextView.typeText(testMessage) + + fillInFields(testMessage, testName, testEmail) - sendButton.tap() - - XCTAssert(widgetButton.waitForExistence(timeout: 1)) + submit() try assertOnlyHookMarkersExist(names: [.onFormClose, .onSubmitSuccess]) XCTAssertEqual(try dictionaryFromSuccessHookFile(), ["message": "UITest user feedback", "email": testEmail, "name": testName]) @@ -157,18 +148,7 @@ extension UserFeedbackUITests { cancelButton.tap() - extrasAreaTabBarButton.tap() - app.buttons["io.sentry.ui-test.button.get-latest-envelope"].tap() - let marshaledDataBase64 = try XCTUnwrap(dataMarshalingField.value as? String) - let data = try XCTUnwrap(Data(base64Encoded: marshaledDataBase64)) - let dict = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any]) - XCTAssertEqual(try XCTUnwrap(dict["event_type"] as? String), "feedback") - XCTAssertEqual(try XCTUnwrap(dict["message"] as? String), testMessage) - XCTAssertEqual(try XCTUnwrap(dict["contact_email"] as? String), testEmail) - XCTAssertEqual(try XCTUnwrap(dict["source"] as? String), "widget") - XCTAssertEqual(try XCTUnwrap(dict["name"] as? String), testName) - XCTAssertNotNil(dict["event_id"]) - XCTAssertEqual(try XCTUnwrap(dict["item_header_type"] as? String), "feedback") + try assertEnvelopeContents(testMessage, testEmail, testName) } func testSubmitFullyFilledForm() throws { @@ -184,17 +164,13 @@ extension UserFeedbackUITests { try assertHookMarkersNotExist() widgetButton.tap() - XCTAssert(nameField.waitForExistence(timeout: 1)) try assertOnlyHookMarkersExist(names: [.onFormOpen]) - messageTextView.tap() let testMessage = "UITest user feedback" - messageTextView.typeText(testMessage) - - sendButton.tap() + fillInFields(testMessage) - XCTAssert(widgetButton.waitForExistence(timeout: 1)) + submit() try assertOnlyHookMarkersExist(names: [.onFormClose, .onSubmitSuccess]) XCTAssertEqual(try dictionaryFromSuccessHookFile(), ["message": "UITest user feedback", "email": testContactEmail, "name": testName]) @@ -248,15 +224,13 @@ extension UserFeedbackUITests { try assertHookMarkersNotExist() widgetButton.tap() - + XCTAssert(sendButton.waitForExistence(timeout: 1)) try assertOnlyHookMarkersExist(names: [.onFormOpen]) messageTextView.tap() messageTextView.typeText("UITest user feedback") - sendButton.tap() - - XCTAssert(widgetButton.waitForExistence(timeout: 1)) + submit() try assertOnlyHookMarkersExist(names: [.onFormClose, .onSubmitSuccess]) XCTAssertEqual(try dictionaryFromSuccessHookFile(), ["name": testName, "message": "UITest user feedback", "email": testContactEmail]) @@ -279,7 +253,7 @@ extension UserFeedbackUITests { try assertHookMarkersNotExist() widgetButton.tap() - + XCTAssert(sendButton.waitForExistence(timeout: 1)) try assertOnlyHookMarkersExist(names: [.onFormOpen]) messageTextView.tap() @@ -316,7 +290,7 @@ extension UserFeedbackUITests { try assertHookMarkersNotExist() widgetButton.tap() - + XCTAssert(sendButton.waitForExistence(timeout: 1)) try assertOnlyHookMarkersExist(names: [.onFormOpen]) messageTextView.tap() @@ -344,11 +318,30 @@ extension UserFeedbackUITests { // MARK: Tests validating screenshot functionality - func testAddingAndRemovingScreenshots() { + func testAddingScreenshots() throws { + launchApp(args: ["--io.sentry.feedback.inject-screenshot"]) + XCTAssert(removeScreenshotButton.isHittable) + + let testMessage = "UITest user feedback" + fillInFields(testMessage) + + submit() + + try assertEnvelopeContents(testMessage, attachments: true) + } + + func testAddingAndRemovingScreenshots() throws { launchApp(args: ["--io.sentry.feedback.inject-screenshot"]) XCTAssert(removeScreenshotButton.isHittable) removeScreenshotButton.tap() XCTAssertFalse(removeScreenshotButton.isHittable) + + let testMessage = "UITest user feedback" + fillInFields(testMessage) + + submit() + + try assertEnvelopeContents(testMessage) } // MARK: Tests validating error cases @@ -361,7 +354,7 @@ extension UserFeedbackUITests { widgetButton.tap() - sendButton.tap() + submit(expectingError: true) XCTAssert(app.staticTexts["Error"].exists) XCTAssert(app.staticTexts["You must provide all required information before submitting. Please check the following field: description."].exists) @@ -386,7 +379,7 @@ extension UserFeedbackUITests { XCTAssertFalse(app.staticTexts["Thy name (Required)"].exists) XCTAssert(app.staticTexts["Thy complaint (Required)"].exists) - sendButton.tap() + submit(expectingError: true) XCTAssert(app.staticTexts["Error"].exists) XCTAssert(app.staticTexts["You must provide all required information before submitting. Please check the following fields: thine email and thy complaint."].exists) @@ -413,7 +406,7 @@ extension UserFeedbackUITests { XCTAssert(app.staticTexts["Thy name (Required)"].exists) XCTAssert(app.staticTexts["Thy complaint (Required)"].exists) - sendButton.tap() + submit(expectingError: true) XCTAssert(app.staticTexts["Error"].exists) XCTAssert(app.staticTexts.element(matching: NSPredicate(format: "label LIKE 'You must provide all required information before submitting. Please check the following fields: thy name, thine email and thy complaint.'")).exists) @@ -432,7 +425,7 @@ extension UserFeedbackUITests { widgetButton.tap() - sendButton.tap() + submit(expectingError: true) XCTAssert(app.staticTexts["Error"].exists) XCTAssert(app.staticTexts["You must provide all required information before submitting. Please check the following field: description."].exists) @@ -457,7 +450,7 @@ extension UserFeedbackUITests { widgetButton.tap() - sendButton.tap() + submit(expectingError: true) XCTAssert(app.staticTexts["Error"].exists) app.buttons["OK"].tap() @@ -468,9 +461,7 @@ extension UserFeedbackUITests { messageTextView.tap() messageTextView.typeText("UITest user feedback") - sendButton.tap() - - XCTAssert(widgetButton.waitForExistence(timeout: 1)) + submit() try assertOnlyHookMarkersExist(names: [.onFormClose, .onSubmitSuccess]) XCTAssertEqual(try dictionaryFromSuccessHookFile(), ["name": testName, "message": "UITest user feedback", "email": testContactEmail]) @@ -518,6 +509,13 @@ extension UserFeedbackUITests { // MARK: Form hook test helpers extension UserFeedbackUITests { + func submit(expectingError: Bool = false) { + sendButton.tap() + if !expectingError { + XCTAssert(widgetButton.waitForExistence(timeout: 1)) + } + } + func path(for marker: HookMarkerFile) throws -> String { let appSupportDirectory = try XCTUnwrap(appSupportDirectory) return "\(appSupportDirectory)/io.sentry/feedback/\(marker.rawValue)" @@ -561,6 +559,47 @@ extension UserFeedbackUITests { app.buttons["io.sentry.ui-test.button.get-application-support-directory"].tap() appSupportDirectory = try XCTUnwrap(dataMarshalingField.value as? String) } + + func fillInFields(_ testMessage: String, _ testName: String? = nil, _ testEmail: String? = nil) { + if let testName = testName { + nameField.tap() + nameField.typeText(testName) + } + + if let testEmail = testEmail { + emailField.tap() + emailField.typeText(testEmail) + } + + messageTextView.tap() + messageTextView.typeText(testMessage) + } + + func assertEnvelopeContents(_ testMessage: String, _ testEmail: String? = nil, _ testName: String? = nil, attachments: Bool = false) throws { + extrasAreaTabBarButton.tap() + app.buttons["io.sentry.ui-test.button.get-latest-envelope"].tap() + let marshaledDataBase64 = try XCTUnwrap(dataMarshalingField.value as? String) + let data = try XCTUnwrap(Data(base64Encoded: marshaledDataBase64)) + let dict = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any]) + XCTAssertEqual(try XCTUnwrap(dict["event_type"] as? String), "feedback") + XCTAssertEqual(try XCTUnwrap(dict["message"] as? String), testMessage) + if let testEmail = testEmail { + XCTAssertEqual(try XCTUnwrap(dict["contact_email"] as? String), testEmail) + } + XCTAssertEqual(try XCTUnwrap(dict["source"] as? String), "widget") + if let testName = testName { + XCTAssertEqual(try XCTUnwrap(dict["name"] as? String), testName) + } + XCTAssertNotNil(dict["event_id"]) + XCTAssertEqual(try XCTUnwrap(dict["item_header_type"] as? String), "feedback") + if attachments { + XCTAssertNotNil(dict["feedback_attachments"]) + let screenshotDataStrings = try XCTUnwrap(dict["feedback_attachments"] as? [String]) + XCTAssertEqual(screenshotDataStrings.count, 1) + let screenshotDataString = try XCTUnwrap(screenshotDataStrings.first) + XCTAssertNotNil(UIImage(data: try XCTUnwrap(Data(base64Encoded: screenshotDataString)))) + } + } } //swiftlint:enable file_length diff --git a/Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift b/Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift index 29eafc0961..d059d68e7c 100644 --- a/Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift @@ -62,15 +62,6 @@ class ErrorsViewController: UIViewController { // It contains all data but mutations only influence the event being sent scope.setTag(value: "value", key: "myTag") } - - if !ProcessInfo.processInfo.arguments.contains("--io.sentry.feedback.auto-inject-widget") { - let alert = UIAlertController(title: "Uh-oh!", message: "There was an error. Would you like to tell us what happened?", preferredStyle: .alert) - alert.addAction(.init(title: "Yes", style: .default, handler: { _ in - SentrySDK.showUserFeedbackForm() - })) - alert.addAction(.init(title: "No", style: .cancel)) - self.present(alert, animated: true) - } } } diff --git a/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift b/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift index b222ff56ea..2f0c2feff3 100644 --- a/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift @@ -207,9 +207,14 @@ class ExtraViewController: UIViewController { } enum EnvelopeContent { - case image(Data) + /// String contents are base64 encoded image data + case image(String) + case rawText(String) case json([String: Any]) + + /// String contents are base64 encoded image data + case feedbackAttachment(String) } func displayError(message: String) { @@ -280,18 +285,29 @@ class ExtraViewController: UIViewController { displayError(message: "\(envelopePath) had no contents.") return nil } + var waitingForFeedbackAttachment = false let parsedEnvelopeContents = envelopeFileContents.split(separator: "\n").map { line in if let imageData = Data(base64Encoded: String(line), options: []) { - return EnvelopeContent.image(imageData) + guard !waitingForFeedbackAttachment else { + waitingForFeedbackAttachment = false + return EnvelopeContent.feedbackAttachment(String(line)) + } + return EnvelopeContent.image(String(line)) } else if let data = line.data(using: .utf8), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + if let type = json["attachment_type"] as? String, type == "event.attachment" { + waitingForFeedbackAttachment = true + } return EnvelopeContent.json(json) } else { return EnvelopeContent.rawText(String(line)) } } let contentsForUITest = parsedEnvelopeContents.reduce(into: [String: Any]()) { result, item in - if case let .json(json) = item { - insertValues(from: json, into: &result) + switch item { + case let .rawText(text): result["text"] = text + case let .image(base64Data): result["scope_images"] = (result["scope_images"] as? [String]) ?? [] + [base64Data] + case let .feedbackAttachment(base64Data): result["feedback_attachments"] = (result["feedback_attachments"] as? [String]) ?? [] + [base64Data] + case let .json(json): insertValues(from: json, into: &result) } } guard let data = try? JSONSerialization.data(withJSONObject: contentsForUITest) else { diff --git a/Samples/iOS-Swift/iOS-Swift/SentrySDKWrapper.swift b/Samples/iOS-Swift/iOS-Swift/SentrySDKWrapper.swift index 2b7daac915..6e6d2e2972 100644 --- a/Samples/iOS-Swift/iOS-Swift/SentrySDKWrapper.swift +++ b/Samples/iOS-Swift/iOS-Swift/SentrySDKWrapper.swift @@ -153,6 +153,7 @@ extension SentrySDKWrapper { } func configureFeedbackForm(config: SentryUserFeedbackFormConfiguration) { + config.useSentryUser = !args.contains("--io.sentry.feedback.dont-use-sentry-user") config.formTitle = "Jank Report" config.isEmailRequired = args.contains("--io.sentry.feedback.require-email") config.isNameRequired = args.contains("--io.sentry.feedback.require-name") @@ -181,7 +182,7 @@ extension SentrySDKWrapper { fontFamily = "ChalkboardSE-Regular" } config.fontFamily = fontFamily - config.outlineStyle = .init(outlineColor: .purple) + config.outlineStyle = .init(color: .purple) config.foreground = .purple config.background = .init(red: 0.95, green: 0.9, blue: 0.95, alpha: 1) config.submitBackground = .orange @@ -199,7 +200,6 @@ extension SentrySDKWrapper { return } - config.useSentryUser = !args.contains("--io.sentry.feedback.dont-use-sentry-user") config.animations = !args.contains("--io.sentry.feedback.no-animations") config.useShakeGesture = true config.showFormForScreenshots = true diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index c3d7fb573f..f61aa31d6a 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -617,7 +617,7 @@ - (void)captureFeedback:(SentryFeedback *)feedback withScope:(SentryScope *)scop alwaysAttachStacktrace:NO]; SentryTraceContext *traceContext = [self getTraceStateWithEvent:preparedEvent withScope:scope]; NSArray *attachments = [[self attachmentsForEvent:preparedEvent scope:scope] - arrayByAddingObjectsFromArray:[feedback attachments]]; + arrayByAddingObjectsFromArray:[feedback attachmentsForEnvelope]]; [self.transportAdapter sendEvent:preparedEvent traceContext:traceContext diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index f4862ee1a4..89ac0501f0 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -421,13 +421,6 @@ + (void)captureFeedback:(SentryFeedback *)feedback [SentrySDK.currentHub captureFeedback:feedback]; } -#if TARGET_OS_IOS && SENTRY_HAS_UIKIT -+ (void)showUserFeedbackForm -{ - // TODO: implement -} -#endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT - + (void)addBreadcrumb:(SentryBreadcrumb *)crumb { [SentrySDK.currentHub addBreadcrumb:crumb]; diff --git a/Sources/Sentry/include/SentrySDK+Private.h b/Sources/Sentry/include/SentrySDK+Private.h index 26bb3a89eb..4b6be64cee 100644 --- a/Sources/Sentry/include/SentrySDK+Private.h +++ b/Sources/Sentry/include/SentrySDK+Private.h @@ -64,19 +64,6 @@ NS_ASSUME_NONNULL_BEGIN */ + (void)captureFeedback:(SentryFeedback *)feedback NS_SWIFT_NAME(capture(feedback:)); -#if TARGET_OS_IOS && SENTRY_HAS_UIKIT -/** - * Display a form to gather information from an end user in the app to send to Sentry as a user - * feedback event. - * @see @c SentryOptions.configureUserFeedback to customize the experience, currently only on iOS. - * @warning This is an experimental feature and may still have bugs. - * @note This is a fully managed user feedback flow; there will be no need to call - * @c SentrySDK.captureUserFeedback . See - * https://docs.sentry.io/platforms/apple/user-feedback/ for more information. - */ -+ (void)showUserFeedbackForm; -#endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT - @end NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift index 28d0d5c1bc..c67d14b437 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackConfiguration.swift @@ -53,12 +53,6 @@ public class SentryUserFeedbackConfiguration: NSObject { */ public var tags: [String: Any]? - /** - * Sets the email and name field text content to `SentryUser.email` and `SentryUser.name`. - * - note: Default: `true` - */ - public var useSentryUser: Bool = true - /** * Called when the managed feedback form is opened. * - note: Default: `nil` diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift index 74d5c94337..e82ac441f2 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift @@ -11,6 +11,14 @@ import UIKit public class SentryUserFeedbackFormConfiguration: NSObject { // MARK: General settings + /** + * Sets the email and name field text content to the values contained in the current scope's + * `SentryUser` instance, if any. + * - seealso: `- [SentrySDK setUser:]` + * - note: Default: `true` + */ + public var useSentryUser: Bool = true + /** * Displays the Sentry logo inside of the form. * - note: Default: `true` @@ -41,16 +49,10 @@ public class SentryUserFeedbackFormConfiguration: NSObject { /** * The label shown next to an input field that is required. - * - note: Default: `"(required)"` + * - note: Default: `"(Required)"` */ public var isRequiredLabel: String = "(Required)" - /** - * The message displayed after a successful feedback submission. - * - note: Default: `"Thank you for your report!"` - */ - public var successMessageText: String = "Thank you for your report!" - // MARK: Screenshots /** @@ -124,7 +126,7 @@ public class SentryUserFeedbackFormConfiguration: NSObject { */ public var emailPlaceholder: String = "your.email@example.org" - public lazy var emailTextFieldAccessibilityLabel = emailPlaceholder + public lazy var emailTextFieldAccessibilityLabel = "Your email address" // MARK: Buttons diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift index 5607ed0118..02b4f0a27f 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackThemeConfiguration.swift @@ -16,13 +16,13 @@ public class SentryUserFeedbackThemeConfiguration: NSObject { public lazy var fontFamily: String? = nil /** - * Font for form input elements. + * Font for form input elements and the widget button label. * - note: Defaults to `UIFont.TextStyle.callout`. */ lazy var font = scaledFont(style: .callout) /** - * Font for main header title of the feedback form. + * Font for the main header title of the feedback form. * - note: Defaults to `UIFont.TextStyle.title1`. */ lazy var headerFont = scaledFont(style: .title1) @@ -88,24 +88,18 @@ public class SentryUserFeedbackThemeConfiguration: NSObject { */ public var buttonBackground: UIColor = UIColor.clear - /** - * Color used for success-related components (such as text color when feedback is submitted successfully). - * - note: Default light mode: `rgb(38, 141, 117)`; dark mode: `rgb(45, 169, 140)` - */ - public var successColor = UIScreen.main.traitCollection.userInterfaceStyle == .dark ? UIColor(red: 45 / 255, green: 169 / 255, blue: 140 / 255, alpha: 1) : UIColor(red: 38 / 255, green: 141 / 255, blue: 117 / 255, alpha: 1) - /** * Color used for error-related components (such as text color when there's an error submitting feedback). * - note: Default light mode: `rgb(223, 51, 56)`; dark mode: `rgb(245, 84, 89)` */ public var errorColor = UIScreen.main.traitCollection.userInterfaceStyle == .dark ? UIColor(red: 245 / 255, green: 84 / 255, blue: 89 / 255, alpha: 1) : UIColor(red: 223 / 255, green: 51 / 255, blue: 56 / 255, alpha: 1) - public struct OutlineStyle: Equatable { + @objc public class SentryFormElementOutlineStyle: NSObject { /** * Outline color for form inputs. * - note: Default: The system default of a UITextField outline with borderStyle of .roundedRect. */ - public var outlineColor = UIColor(white: 204 / 255, alpha: 1) + public var color = UIColor(white: 204 / 255, alpha: 1) /** * Outline corner radius for form input elements. @@ -119,8 +113,8 @@ public class SentryUserFeedbackThemeConfiguration: NSObject { */ public var outlineWidth: CGFloat = 0.5 - public init(outlineColor: UIColor = UIColor(white: 204 / 255, alpha: 1), cornerRadius: CGFloat = 5, outlineWidth: CGFloat = 0.5) { - self.outlineColor = outlineColor + @objc public init(color: UIColor = UIColor(white: 204 / 255, alpha: 1), cornerRadius: CGFloat = 5, outlineWidth: CGFloat = 0.5) { + self.color = color self.cornerRadius = cornerRadius self.outlineWidth = outlineWidth } @@ -129,17 +123,22 @@ public class SentryUserFeedbackThemeConfiguration: NSObject { /** * - note: We need to keep a reference to a default instance of this for comparison purposes later. We don't use the default to give UITextFields a default style, instead, we use `UITextField.BorderStyle.roundedRect` if `SentryUserFeedbackThemeConfiguration.outlineStyle == defaultOutlineStyle`. */ - let defaultOutlineStyle = OutlineStyle() + let defaultOutlineStyle = SentryFormElementOutlineStyle() /** * Options for styling the outline of input elements and buttons in the feedback form. */ - public lazy var outlineStyle: OutlineStyle = defaultOutlineStyle + public lazy var outlineStyle: SentryFormElementOutlineStyle = defaultOutlineStyle /** * Background color to use for text inputs in the feedback form. */ public var inputBackground: UIColor = UIColor.secondarySystemBackground + + /** + * Background color to use for text inputs in the feedback form. + */ + public var inputForeground: UIColor = UIScreen.main.traitCollection.userInterfaceStyle == .dark ? UIColor.lightText : UIColor.darkText } #endif // os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift index f3f4890100..b29bf548ab 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift @@ -3,7 +3,14 @@ import Foundation @objcMembers class SentryFeedback: NSObject { - enum Source: String { + @objc enum SentryFeedbackSource: Int { + public var serialize: String { + switch self { + case .widget: return "widget" + case .custom: return "custom" + } + } + case widget case custom } @@ -11,24 +18,26 @@ class SentryFeedback: NSObject { var name: String? var email: String? var message: String - var source: Source + var source: SentryFeedbackSource let eventId: SentryId - /// PNG data for the screenshot image - var screenshot: Data? + /// Data objects for any attachments. Currently the web UI only supports showing one attached image, like for a screenshot. + private var attachments: [Data]? /// The event id that this feedback is associated with, like a crash report. - var associatedEventId: String? + var associatedEventId: SentryId? - /// - parameter screenshot Image encoded as PNG data. - init(message: String, name: String?, email: String?, source: Source = .widget, associatedEventId: String? = nil, screenshot: Data? = nil) { + /// - parameters: + /// - associatedEventId The ID for an event you'd like associated with the feedback. + /// - attachments Data objects for any attachments. Currently the web UI only supports showing one attached image, like for a screenshot. + @objc init(message: String, name: String?, email: String?, source: SentryFeedbackSource = .widget, associatedEventId: SentryId? = nil, attachments: [Data]? = nil) { self.eventId = SentryId() self.name = name self.email = email self.message = message self.source = source self.associatedEventId = associatedEventId - self.screenshot = screenshot + self.attachments = attachments super.init() } } @@ -45,9 +54,9 @@ extension SentryFeedback: SentrySerializable { dict["contact_email"] = email } if let associatedEventId = associatedEventId { - dict["associated_event_id"] = associatedEventId + dict["associated_event_id"] = associatedEventId.sentryIdString } - dict["source"] = source.rawValue + dict["source"] = source.serialize return dict } @@ -66,8 +75,8 @@ extension SentryFeedback { if let email = email { dict["email"] = email } - if let screenshot = screenshot { - dict["attachments"] = [screenshot] + if let attachments = attachments { + dict["attachments"] = attachments } return dict } @@ -75,9 +84,9 @@ extension SentryFeedback { /** * - note: Currently there is only a single attachment possible, for the screenshot, of which there can be only one. */ - func attachments() -> [Attachment] { + func attachmentsForEnvelope() -> [Attachment] { var items = [Attachment]() - if let screenshot = screenshot { + if let screenshot = attachments?.first { items.append(Attachment(data: screenshot, filename: "screenshot.png", contentType: "application/png")) } return items diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormViewModel.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormViewModel.swift index cae3058b5c..497ce7ec84 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormViewModel.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormViewModel.swift @@ -70,7 +70,7 @@ class SentryUserFeedbackFormViewModel: NSObject { field.delegate = controller field.autocapitalizationType = .words field.returnKeyType = .done - if config.useSentryUser { + if config.formConfig.useSentryUser { field.text = sentry_getCurrentUser()?.name } return field @@ -91,7 +91,7 @@ class SentryUserFeedbackFormViewModel: NSObject { field.keyboardType = .emailAddress field.autocapitalizationType = .none field.returnKeyType = .done - if config.useSentryUser { + if config.formConfig.useSentryUser { field.text = sentry_getCurrentUser()?.email } return field @@ -336,12 +336,13 @@ extension SentryUserFeedbackFormViewModel { [fullNameTextField, emailTextField].forEach { $0.font = config.theme.font $0.adjustsFontForContentSizeCategory = true + $0.textColor = config.theme.inputForeground if config.theme.outlineStyle == config.theme.defaultOutlineStyle { $0.borderStyle = .roundedRect } else { $0.layer.cornerRadius = config.theme.outlineStyle.cornerRadius $0.layer.borderWidth = config.theme.outlineStyle.outlineWidth - $0.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor + $0.layer.borderColor = config.theme.outlineStyle.color.cgColor } } @@ -349,6 +350,8 @@ extension SentryUserFeedbackFormViewModel { $0.backgroundColor = config.theme.inputBackground } + messageTextView.textColor = config.theme.inputForeground + [fullNameLabel, emailLabel, messageLabel].forEach { $0.font = config.theme.titleFont $0.adjustsFontForContentSizeCategory = true @@ -362,7 +365,7 @@ extension SentryUserFeedbackFormViewModel { [submitButton, removeScreenshotButton, cancelButton, messageTextView].forEach { $0.layer.cornerRadius = config.theme.outlineStyle.cornerRadius $0.layer.borderWidth = config.theme.outlineStyle.outlineWidth - $0.layer.borderColor = config.theme.outlineStyle.outlineColor.cgColor + $0.layer.borderColor = config.theme.outlineStyle.color.cgColor } [removeScreenshotButton, cancelButton].forEach { @@ -453,7 +456,11 @@ extension SentryUserFeedbackFormViewModel { } func feedbackObject() -> SentryFeedback { - SentryFeedback(message: messageTextView.text, name: fullNameTextField.text, email: emailTextField.text, screenshot: screenshotImageView.image?.pngData()) + var attachmentDatas: [Data]? + if let image = screenshotImageView.image, let data = image.pngData() { + attachmentDatas = [data] + } + return SentryFeedback(message: messageTextView.text, name: fullNameTextField.text, email: emailTextField.text, attachments: attachmentDatas) } } diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonView.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonView.swift index a9a3f952a1..b7e088ff58 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonView.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidgetButtonView.swift @@ -167,10 +167,10 @@ class SentryUserFeedbackWidgetButtonView: UIView { if UIScreen.main.traitCollection.userInterfaceStyle == .dark { lozengeLayer.fillColor = config.darkTheme.background.cgColor - lozengeLayer.strokeColor = config.darkTheme.outlineStyle.outlineColor.cgColor + lozengeLayer.strokeColor = config.darkTheme.outlineStyle.color.cgColor } else { lozengeLayer.fillColor = config.theme.background.cgColor - lozengeLayer.strokeColor = config.theme.outlineStyle.outlineColor.cgColor + lozengeLayer.strokeColor = config.theme.outlineStyle.color.cgColor } let iconSizeDifference = (scaledIconSize - svgSize) / 2 diff --git a/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift b/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift index d8b8d3da6d..35b232f301 100644 --- a/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift +++ b/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift @@ -29,7 +29,7 @@ class SentryFeedbackTests: XCTestCase { } func testSerializeWithAllFields() throws { - let sut = SentryFeedback(message: "Test feedback message", name: "Test feedback provider", email: "test-feedback-provider@sentry.io", screenshot: Data()) + let sut = SentryFeedback(message: "Test feedback message", name: "Test feedback provider", email: "test-feedback-provider@sentry.io", attachments: [Data()]) let serialization = sut.serialize() XCTAssertEqual(try XCTUnwrap(serialization["message"] as? String), "Test feedback message") @@ -37,7 +37,40 @@ class SentryFeedbackTests: XCTestCase { XCTAssertEqual(try XCTUnwrap(serialization["contact_email"] as? String), "test-feedback-provider@sentry.io") XCTAssertEqual(try XCTUnwrap(serialization["source"] as? String), "widget") - let attachments = sut.attachments() + let attachments = sut.attachmentsForEnvelope() + XCTAssertEqual(attachments.count, 1) + XCTAssertEqual(try XCTUnwrap(attachments.first).filename, "screenshot.png") + XCTAssertEqual(try XCTUnwrap(attachments.first).contentType, "application/png") + } + + func testSerializeCustomFeedback() throws { + let sut = SentryFeedback(message: "Test feedback message", name: "Test feedback provider", email: "test-feedback-provider@sentry.io", source: .custom, attachments: [Data()]) + + let serialization = sut.serialize() + XCTAssertEqual(try XCTUnwrap(serialization["message"] as? String), "Test feedback message") + XCTAssertEqual(try XCTUnwrap(serialization["name"] as? String), "Test feedback provider") + XCTAssertEqual(try XCTUnwrap(serialization["contact_email"] as? String), "test-feedback-provider@sentry.io") + XCTAssertEqual(try XCTUnwrap(serialization["source"] as? String), "custom") + + let attachments = sut.attachmentsForEnvelope() + XCTAssertEqual(attachments.count, 1) + XCTAssertEqual(try XCTUnwrap(attachments.first).filename, "screenshot.png") + XCTAssertEqual(try XCTUnwrap(attachments.first).contentType, "application/png") + } + + func testSerializeWithAssociatedEventID() throws { + let eventID = SentryId() + + let sut = SentryFeedback(message: "Test feedback message", name: "Test feedback provider", email: "test-feedback-provider@sentry.io", source: .custom, associatedEventId: eventID, attachments: [Data()]) + + let serialization = sut.serialize() + XCTAssertEqual(try XCTUnwrap(serialization["message"] as? String), "Test feedback message") + XCTAssertEqual(try XCTUnwrap(serialization["name"] as? String), "Test feedback provider") + XCTAssertEqual(try XCTUnwrap(serialization["contact_email"] as? String), "test-feedback-provider@sentry.io") + XCTAssertEqual(try XCTUnwrap(serialization["source"] as? String), "custom") + XCTAssertEqual(try XCTUnwrap(serialization["associated_event_id"] as? String), eventID.sentryIdString) + + let attachments = sut.attachmentsForEnvelope() XCTAssertEqual(attachments.count, 1) XCTAssertEqual(try XCTUnwrap(attachments.first).filename, "screenshot.png") XCTAssertEqual(try XCTUnwrap(attachments.first).contentType, "application/png") @@ -52,7 +85,7 @@ class SentryFeedbackTests: XCTestCase { XCTAssertNil(serialization["contact_email"]) XCTAssertEqual(try XCTUnwrap(serialization["source"] as? String), "widget") - let attachments = sut.attachments() + let attachments = sut.attachmentsForEnvelope() XCTAssertEqual(attachments.count, 0) }