diff --git a/CHANGELOG.md b/CHANGELOG.md index 09bd3e00ef3..4f42a6dc0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Feature + +- Add options to customize UserFeedback error messages (#6790) + ### Fixes - Ensure SentrySDK.close resets everything on the main thread (#6907) diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift index 04199a023c5..0316c08831c 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift @@ -288,6 +288,10 @@ extension SentrySDKWrapper { config.messageLabel = "Thy complaint" config.emailLabel = "Thine email" config.nameLabel = "Thy name" + config.unexpectedErrorText = "Santry doesn't know how to process this error" + config.validationErrorMessage = { multipleErrors in + return "You got \(multipleErrors ? "many" : "an" ) error\(multipleErrors ? "s" : "") in this form:" + } } func configureFeedbackTheme(config: SentryUserFeedbackThemeConfiguration) { diff --git a/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift b/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift index 38cad195d56..9637073c33a 100644 --- a/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift +++ b/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift @@ -414,7 +414,7 @@ extension UserFeedbackUITests { 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) + XCTAssert(app.staticTexts["You got many errors in this form: thine email and thy complaint."].exists) app.buttons["OK"].tap() @@ -441,7 +441,7 @@ extension UserFeedbackUITests { 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) + XCTAssert(app.staticTexts.element(matching: NSPredicate(format: "label LIKE 'You got many errors in this form: thy name, thine email and thy complaint.'")).exists) app.buttons["OK"].tap() diff --git a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift index 1d97b320ac9..5121fe5e929 100644 --- a/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift +++ b/Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift @@ -155,6 +155,20 @@ public final class SentryUserFeedbackFormConfiguration: NSObject { func fullLabelText(labelText: String, required: Bool) -> String { required ? labelText + " " + isRequiredLabel : labelText } + + /** + * Message shown to the user when an unexpected error happens while submitting feedback. + * - note: Default: `"Unexpected client error."` + */ + public var unexpectedErrorText: String = "Unexpected client error." + + /** + * Message shown to the user when the form fails the validation. + * - note: Default: `"You must provide all required information before submitting. Please check the following field(s)"` + */ + public var validationErrorMessage: (Bool) -> String = { multipleErrors in + return "You must provide all required information before submitting. Please check the following field\(multipleErrors ? "s" : ""):" + } } #endif // os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormController.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormController.swift index ccc005aa14d..9ed7ff57794 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormController.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormController.swift @@ -86,13 +86,14 @@ extension SentryUserFeedbackFormController: SentryUserFeedbackFormViewModelDeleg } } - guard case let SentryUserFeedbackFormViewModel.InputError.validationError(missing) = error else { + guard case let SentryUserFeedbackFormViewModel.InputError.validationError(missing, _) = error, + let errorDescription = error.errorDescription else { SentrySDKLog.warning("Unexpected error type.") - presentAlert(message: "Unexpected client error.", errorCode: 2, info: [NSLocalizedDescriptionKey: "Client error: ."]) + presentAlert(message: config.formConfig.unexpectedErrorText, errorCode: 2, info: [NSLocalizedDescriptionKey: "Client error: ."]) return } - presentAlert(message: error.description, errorCode: 1, info: ["missing_fields": missing, NSLocalizedDescriptionKey: "The user did not complete the feedback form."]) + presentAlert(message: errorDescription, errorCode: 1, info: ["missing_fields": missing, NSLocalizedDescriptionKey: "The user did not complete the feedback form."]) } } diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormViewModel.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormViewModel.swift index 6ead962c80f..a5ebfe4cf3a 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormViewModel.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormViewModel.swift @@ -325,7 +325,7 @@ extension SentryUserFeedbackFormViewModel { func updateSubmitButtonAccessibilityHint() { switch validate() { case .success(let hint): submitButton.accessibilityHint = hint - case .failure(let error): submitButton.accessibilityHint = error.description + case .failure(let error): submitButton.accessibilityHint = error.errorDescription } } @@ -433,23 +433,32 @@ extension SentryUserFeedbackFormViewModel { } guard missing.isEmpty else { - let result = SentryUserFeedbackFormValidation.failure(InputError.validationError(missingFields: missing)) + let localizedError = InputError.buildDescriptionFor(missingFields: missing, validationErrorMessage: config.formConfig.validationErrorMessage) + let result = SentryUserFeedbackFormValidation.failure(InputError.validationError(missingFields: missing, localizedError: localizedError)) return result } return SentryUserFeedbackFormValidation.success(hint.joined(separator: " ").appending(".")) } - enum InputError: Error { - case validationError(missingFields: [String]) + enum InputError: LocalizedError { + case validationError(missingFields: [String], localizedError: String) var description: String { switch self { - case .validationError(let missingFields): - let list = missingFields.count == 1 ? missingFields[0] : missingFields[0 ..< missingFields.count - 1].joined(separator: ", ") + " and " + missingFields[missingFields.count - 1] - return "You must provide all required information before submitting. Please check the following field\(missingFields.count > 1 ? "s" : ""): \(list)." + case .validationError(_, let localizedError): + return localizedError } } + + var errorDescription: String? { + return description + } + + static func buildDescriptionFor(missingFields: [String], validationErrorMessage: (Bool) -> String) -> String { + let list = missingFields.count == 1 ? missingFields[0] : missingFields[0 ..< missingFields.count - 1].joined(separator: ", ") + " and " + missingFields[missingFields.count - 1] + return "\(validationErrorMessage(missingFields.count > 1 )) \(list)." + } } func feedbackObject() -> SentryFeedback { diff --git a/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift b/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift index 1b89fea3111..fb29a03d588 100644 --- a/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift +++ b/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift @@ -183,7 +183,7 @@ class SentryFeedbackTests: XCTestCase { (config: (requiresName: true, requiresEmail: true, nameInput: "tester", emailInput: "test@email.value", messageInput: "Test message", includeScreenshot: true), shouldValidate: true, expectedSubmitButtonAccessibilityHint: "Will submit feedback for tester at test@email.value including attached screenshot with message: Test message.") ] - func testSubmitButtonAccessibilityHint() { + func testSubmitButtonAccessibilityHint() throws { for input in inputCombinations { let config = SentryUserFeedbackConfiguration() config.configureForm = { @@ -204,7 +204,8 @@ class SentryFeedbackTests: XCTestCase { XCTAssert(input.shouldValidate) XCTAssertEqual(hint, input.expectedSubmitButtonAccessibilityHint, testCaseDescription()) case .failure(let error): - XCTAssertFalse(input.shouldValidate, error.description + "; " + testCaseDescription()) + let errorDescription = try XCTUnwrap(error.errorDescription) + XCTAssertFalse(input.shouldValidate, errorDescription + "; " + testCaseDescription()) } } diff --git a/sdk_api.json b/sdk_api.json index b764e43a166..32385e9f07a 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -56154,6 +56154,197 @@ } ] }, + { + "kind": "Var", + "name": "unexpectedErrorText", + "printedName": "unexpectedErrorText", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Var", + "usr": "c:@M@Sentry@objc(cs)SentryUserFeedbackFormConfiguration(py)unexpectedErrorText", + "mangledName": "$s6Sentry0A29UserFeedbackFormConfigurationC19unexpectedErrorTextSSvp", + "moduleName": "Sentry", + "declAttributes": [ + "Final", + "ObjC", + "HasStorage" + ], + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryUserFeedbackFormConfiguration(im)unexpectedErrorText", + "mangledName": "$s6Sentry0A29UserFeedbackFormConfigurationC19unexpectedErrorTextSSvg", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "Final", + "ObjC" + ], + "accessorKind": "get" + }, + { + "kind": "Accessor", + "name": "Set", + "printedName": "Set()", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryUserFeedbackFormConfiguration(im)setUnexpectedErrorText:", + "mangledName": "$s6Sentry0A29UserFeedbackFormConfigurationC19unexpectedErrorTextSSvs", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "Final", + "ObjC" + ], + "accessorKind": "set" + } + ] + }, + { + "kind": "Var", + "name": "validationErrorMessage", + "printedName": "validationErrorMessage", + "children": [ + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(Swift.Bool) -> Swift.String", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ] + } + ], + "declKind": "Var", + "usr": "c:@M@Sentry@objc(cs)SentryUserFeedbackFormConfiguration(py)validationErrorMessage", + "mangledName": "$s6Sentry0A29UserFeedbackFormConfigurationC22validationErrorMessageySSSbcvp", + "moduleName": "Sentry", + "declAttributes": [ + "Final", + "ObjC", + "HasStorage" + ], + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(Swift.Bool) -> Swift.String", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ] + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryUserFeedbackFormConfiguration(im)validationErrorMessage", + "mangledName": "$s6Sentry0A29UserFeedbackFormConfigurationC22validationErrorMessageySSSbcvg", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "Final", + "ObjC" + ], + "accessorKind": "get" + }, + { + "kind": "Accessor", + "name": "Set", + "printedName": "Set()", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(Swift.Bool) -> Swift.String", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ] + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryUserFeedbackFormConfiguration(im)setValidationErrorMessage:", + "mangledName": "$s6Sentry0A29UserFeedbackFormConfigurationC22validationErrorMessageySSSbcvs", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "Final", + "ObjC" + ], + "accessorKind": "set" + } + ] + }, { "kind": "Constructor", "name": "init",