Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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."])
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class SentryFeedbackTests: XCTestCase {
(config: (requiresName: true, requiresEmail: true, nameInput: "tester", emailInput: "[email protected]", messageInput: "Test message", includeScreenshot: true), shouldValidate: true, expectedSubmitButtonAccessibilityHint: "Will submit feedback for tester at [email protected] including attached screenshot with message: Test message.")
]

func testSubmitButtonAccessibilityHint() {
func testSubmitButtonAccessibilityHint() throws {
for input in inputCombinations {
let config = SentryUserFeedbackConfiguration()
config.configureForm = {
Expand All @@ -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())
}

}
Expand Down
191 changes: 191 additions & 0 deletions sdk_api.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading