Skip to content

Commit aa15c88

Browse files
antoniskrystofwoldrichlucas-zimerman
authored
(2.4) feat(feedback-ui): Add screenshots (#4338)
* Update the client implementation to use the new capture feedback js api * Updates SDK API * Adds new feedback button in the sample * Adds changelog * Removes unused mock * Update CHANGELOG.md Co-authored-by: Krystof Woldrich <[email protected]> * Directly use captureFeedback from sentry/core * Use import from core * Fixes imports order lint issue * Fixes build issue * Adds captureFeedback tests from sentry-javascript * Update CHANGELOG.md * Only deprecate client captureUserFeedback * Add simple form UI * Adds basic form functionality * Update imports * Update imports * Remove useState hook to avoid multiple react instances issues * Move types and styles in different files * Removes attachment button to be added back separately along with the implementation * Add basic field validation * Adds changelog * Updates changelog * Updates changelog * Trim whitespaces from the submitted feedback * Adds tests * Adds attachment button UI * Adds changelog * Add attachment handling based on the client implementation * Reduce render method complexity * Adds test for attachment button visibility * Format code * Pick image with react-native-image-picker * Convert base64 string to Uint8Array before sending * Updates changelog * Renames FeedbackFormScreen to FeedbackForm * Add beta label * Extract default text to constants * Moves constant to a separate file and aligns naming with JS * Adds input text labels * Close screen before sending the feedback to minimise wait time Co-authored-by: LucasZF <[email protected]> * Rename file for consistency * Flatten configuration hierarchy and clean up * Align required values with JS * Use Sentry user email and name when set * Simplifies email validation * Show success alert message * Aligns naming with JS and unmounts the form by default * Use the minimum config without props in the changelog * Adds development not for unimplemented function * Show email and name conditionally * Adds sentry branding (png logo) * Adds sentry logo resource * Add assets in module exports * Revert "Add assets in module exports" This reverts commit 5292475. * Revert "Adds sentry logo resource" This reverts commit d6e9229. * Revert "Adds sentry branding (png logo)" This reverts commit 8c56753. * Add last event id * Mock lastEventId * Remove changelog * Reverse unrelated change * Adds beta note in the changelog * Updates changelog * Align colors with JS * Update CHANGELOG.md Co-authored-by: Krystof Woldrich <[email protected]> * Update CHANGELOG.md Co-authored-by: Krystof Woldrich <[email protected]> * Update CHANGELOG.md Co-authored-by: Krystof Woldrich <[email protected]> * Use regular fonts for both buttons * Handle keyboard properly * Adds an option on whether the email should be validated * Merge properties only once * Loads current user data on form construction * Remove unneeded extra padding * Fix background color issue * Fixes changelog typo * Updates styles background color Co-authored-by: Krystof Woldrich <[email protected]> * Use defaultProps * Correct defaultProps * Adds test to verify when getUser is called * Add default value in doc comment Co-authored-by: LucasZF <[email protected]> * Add a more clear doc comment Co-authored-by: LucasZF <[email protected]> * (2.2) feat: Add Feedback Form UI Branding logo (#4357) * Adds sentry branding logo as a base64 encoded png --------- Co-authored-by: LucasZF <[email protected]> * Autoinject feedback form (#4370) * Align changelog entry * Update changelog * Use AddScreenshot naming * Allow only Uint8Array for screenshots * Rename callback parameter * Adds snapshot tests for screenshot button * Rename screenshot button for clarity * Use a library to get the Uint8Array --------- Co-authored-by: Krystof Woldrich <[email protected]> Co-authored-by: LucasZF <[email protected]>
1 parent 93b770e commit aa15c88

File tree

10 files changed

+1192
-15
lines changed

10 files changed

+1192
-15
lines changed

packages/core/src/js/feedback/FeedbackForm.styles.ts

+11
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ const defaultStyles: FeedbackFormStyles = {
3939
textAlignVertical: 'top',
4040
color: FORGROUND_COLOR,
4141
},
42+
screenshotButton: {
43+
backgroundColor: '#eee',
44+
padding: 15,
45+
borderRadius: 5,
46+
marginBottom: 20,
47+
alignItems: 'center',
48+
},
49+
screenshotText: {
50+
color: '#333',
51+
fontSize: 16,
52+
},
4253
submitButton: {
4354
backgroundColor: PURPLE,
4455
paddingVertical: 15,

packages/core/src/js/feedback/FeedbackForm.tsx

+30-2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
6868
return;
6969
}
7070

71+
const attachments = this.state.filename && this.state.attachment
72+
? [
73+
{
74+
filename: this.state.filename,
75+
data: this.state.attachment,
76+
},
77+
]
78+
: undefined;
79+
7180
const eventId = lastEventId();
7281
const userFeedback: SendFeedbackParams = {
7382
message: trimmedDescription,
@@ -78,7 +87,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
7887

7988
try {
8089
this.setState({ isVisible: false });
81-
captureFeedback(userFeedback);
90+
captureFeedback(userFeedback, attachments ? { attachments } : undefined);
8291
onSubmitSuccess({ name: trimmedName, email: trimmedEmail, message: trimmedDescription, attachments: undefined });
8392
Alert.alert(text.successMessageText);
8493
onFormSubmitted();
@@ -90,6 +99,17 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
9099
}
91100
};
92101

102+
public onScreenshotButtonPress: () => void = () => {
103+
if (!this.state.filename && !this.state.attachment) {
104+
const { onAddScreenshot } = { ...defaultConfiguration, ...this.props };
105+
onAddScreenshot((filename: string, attachement: Uint8Array) => {
106+
this.setState({ filename, attachment: attachement });
107+
});
108+
} else {
109+
this.setState({ filename: undefined, attachment: undefined });
110+
}
111+
}
112+
93113
/**
94114
* Renders the feedback form screen.
95115
*/
@@ -167,7 +187,15 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
167187
onChangeText={(value) => this.setState({ description: value })}
168188
multiline
169189
/>
170-
190+
{config.enableScreenshot && (
191+
<TouchableOpacity style={styles.screenshotButton} onPress={this.onScreenshotButtonPress}>
192+
<Text style={styles.screenshotText}>
193+
{!this.state.filename && !this.state.attachment
194+
? text.addScreenshotButtonLabel
195+
: text.removeScreenshotButtonLabel}
196+
</Text>
197+
</TouchableOpacity>
198+
)}
171199
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
172200
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
173201
</TouchableOpacity>

packages/core/src/js/feedback/FeedbackForm.types.ts

+25
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ export interface FeedbackGeneralConfiguration {
4444
*/
4545
showName?: boolean;
4646

47+
/**
48+
* This flag determines whether the "Add Screenshot" button is displayed
49+
* @default false
50+
*/
51+
enableScreenshot?: boolean;
52+
4753
/**
4854
* Fill in email/name input fields with Sentry user context if it exists.
4955
* The value of the email/name keys represent the properties of your user context.
@@ -113,6 +119,16 @@ export interface FeedbackTextConfiguration {
113119
*/
114120
isRequiredLabel?: string;
115121

122+
/**
123+
* The label for the button that adds a screenshot and renders the image editor
124+
*/
125+
addScreenshotButtonLabel?: string;
126+
127+
/**
128+
* The label for the button that removes a screenshot and hides the image editor
129+
*/
130+
removeScreenshotButtonLabel?: string;
131+
116132
/**
117133
* The title of the error dialog
118134
*/
@@ -148,6 +164,11 @@ export interface FeedbackCallbacks {
148164
*/
149165
onFormClose?: () => void;
150166

167+
/**
168+
* Callback when a screenshot is added
169+
*/
170+
onAddScreenshot?: (attachFile: (filename: string, data: Uint8Array) => void) => void;
171+
151172
/**
152173
* Callback when feedback is successfully submitted
153174
*
@@ -179,6 +200,8 @@ export interface FeedbackFormStyles {
179200
submitText?: TextStyle;
180201
cancelButton?: ViewStyle;
181202
cancelText?: TextStyle;
203+
screenshotButton?: ViewStyle;
204+
screenshotText?: TextStyle;
182205
titleContainer?: ViewStyle;
183206
sentryLogo?: ImageStyle;
184207
}
@@ -191,4 +214,6 @@ export interface FeedbackFormState {
191214
name: string;
192215
email: string;
193216
description: string;
217+
filename?: string;
218+
attachment?: string | Uint8Array;
194219
}

packages/core/src/js/feedback/defaults.ts

+10
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const ERROR_TITLE = 'Error';
1616
const FORM_ERROR = 'Please fill out all required fields.';
1717
const EMAIL_ERROR = 'Please enter a valid email address.';
1818
const SUCCESS_MESSAGE_TEXT = 'Thank you for your report!';
19+
const ADD_SCREENSHOT_LABEL = 'Add a screenshot';
20+
const REMOVE_SCREENSHOT_LABEL = 'Remove screenshot';
1921
const GENERIC_ERROR_TEXT = 'Unable to send feedback due to an unexpected error.';
2022

2123
export const defaultConfiguration: Partial<FeedbackFormProps> = {
@@ -31,6 +33,11 @@ export const defaultConfiguration: Partial<FeedbackFormProps> = {
3133
);
3234
}
3335
},
36+
onAddScreenshot: (_: (filename: string, data: Uint8Array) => void) => {
37+
if (__DEV__) {
38+
Alert.alert('Development note', 'onAddScreenshot callback is not implemented.');
39+
}
40+
},
3441
onSubmitSuccess: () => {
3542
// Does nothing by default
3643
},
@@ -53,6 +60,7 @@ export const defaultConfiguration: Partial<FeedbackFormProps> = {
5360
isNameRequired: false,
5461
showEmail: true,
5562
showName: true,
63+
enableScreenshot: false,
5664

5765
// FeedbackTextConfiguration
5866
cancelButtonLabel: CANCEL_BUTTON_LABEL,
@@ -69,5 +77,7 @@ export const defaultConfiguration: Partial<FeedbackFormProps> = {
6977
formError: FORM_ERROR,
7078
emailError: EMAIL_ERROR,
7179
successMessageText: SUCCESS_MESSAGE_TEXT,
80+
addScreenshotButtonLabel: ADD_SCREENSHOT_LABEL,
81+
removeScreenshotButtonLabel: REMOVE_SCREENSHOT_LABEL,
7282
genericError: GENERIC_ERROR_TEXT,
7383
};

packages/core/test/feedback/FeedbackForm.test.tsx

+45-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { FeedbackForm } from '../../src/js/feedback/FeedbackForm';
77
import type { FeedbackFormProps, FeedbackFormStyles } from '../../src/js/feedback/FeedbackForm.types';
88

99
const mockOnFormClose = jest.fn();
10+
const mockOnAddScreenshot = jest.fn();
1011
const mockOnSubmitSuccess = jest.fn();
1112
const mockOnFormSubmitted = jest.fn();
1213
const mockOnSubmitError = jest.fn();
@@ -28,9 +29,11 @@ jest.mock('@sentry/core', () => ({
2829

2930
const defaultProps: FeedbackFormProps = {
3031
onFormClose: mockOnFormClose,
32+
onAddScreenshot: mockOnAddScreenshot,
3133
onSubmitSuccess: mockOnSubmitSuccess,
3234
onFormSubmitted: mockOnFormSubmitted,
3335
onSubmitError: mockOnSubmitError,
36+
addScreenshotButtonLabel: 'Add Screenshot',
3437
formTitle: 'Feedback Form',
3538
nameLabel: 'Name Label',
3639
namePlaceholder: 'Name Placeholder',
@@ -84,6 +87,13 @@ const customStyles: FeedbackFormStyles = {
8487
color: '#ff0000',
8588
fontSize: 10,
8689
},
90+
screenshotButton: {
91+
backgroundColor: '#00ff00',
92+
},
93+
screenshotText: {
94+
color: '#0000ff',
95+
fontSize: 13,
96+
},
8797
};
8898

8999
describe('FeedbackForm', () => {
@@ -107,8 +117,24 @@ describe('FeedbackForm', () => {
107117
expect(toJSON()).toMatchSnapshot();
108118
});
109119

120+
it('matches the snapshot with default configuration and screenshot button', () => {
121+
const { toJSON } = render(<FeedbackForm enableScreenshot={true}/>);
122+
expect(toJSON()).toMatchSnapshot();
123+
});
124+
125+
it('matches the snapshot with custom texts and screenshot button', () => {
126+
const { toJSON } = render(<FeedbackForm {...defaultProps} enableScreenshot={true}/>);
127+
expect(toJSON()).toMatchSnapshot();
128+
});
129+
130+
it('matches the snapshot with custom styles and screenshot button', () => {
131+
const customStyleProps = {styles: customStyles};
132+
const { toJSON } = render(<FeedbackForm {...customStyleProps} enableScreenshot={true}/>);
133+
expect(toJSON()).toMatchSnapshot();
134+
});
135+
110136
it('renders correctly', () => {
111-
const { getByPlaceholderText, getByText, getByTestId } = render(<FeedbackForm {...defaultProps} />);
137+
const { getByPlaceholderText, getByText, getByTestId, queryByText } = render(<FeedbackForm {...defaultProps} />);
112138

113139
expect(getByText(defaultProps.formTitle)).toBeTruthy();
114140
expect(getByTestId('sentry-logo')).toBeTruthy(); // default showBranding is true
@@ -118,10 +144,17 @@ describe('FeedbackForm', () => {
118144
expect(getByPlaceholderText(defaultProps.emailPlaceholder)).toBeTruthy();
119145
expect(getByText(`${defaultProps.messageLabel } ${ defaultProps.isRequiredLabel}`)).toBeTruthy();
120146
expect(getByPlaceholderText(defaultProps.messagePlaceholder)).toBeTruthy();
147+
expect(queryByText(defaultProps.addScreenshotButtonLabel)).toBeNull(); // default false
121148
expect(getByText(defaultProps.submitButtonLabel)).toBeTruthy();
122149
expect(getByText(defaultProps.cancelButtonLabel)).toBeTruthy();
123150
});
124151

152+
it('renders attachment button when the enableScreenshot is true', () => {
153+
const { getByText } = render(<FeedbackForm {...defaultProps} enableScreenshot={true} />);
154+
155+
expect(getByText(defaultProps.addScreenshotButtonLabel)).toBeTruthy();
156+
});
157+
125158
it('does not render the sentry logo when showBranding is false', () => {
126159
const { queryByTestId } = render(<FeedbackForm {...defaultProps} showBranding={false} />);
127160

@@ -188,7 +221,7 @@ describe('FeedbackForm', () => {
188221
message: 'This is a feedback message.',
189222
name: 'John Doe',
190223
191-
});
224+
}, undefined);
192225
});
193226
});
194227

@@ -270,6 +303,16 @@ describe('FeedbackForm', () => {
270303
});
271304
});
272305

306+
it('calls onAddScreenshot when the screenshot button is pressed', async () => {
307+
const { getByText } = render(<FeedbackForm {...defaultProps} enableScreenshot={true} />);
308+
309+
fireEvent.press(getByText(defaultProps.addScreenshotButtonLabel));
310+
311+
await waitFor(() => {
312+
expect(mockOnAddScreenshot).toHaveBeenCalled();
313+
});
314+
});
315+
273316
it('calls onFormClose when the cancel button is pressed', () => {
274317
const { getByText } = render(<FeedbackForm {...defaultProps} />);
275318

0 commit comments

Comments
 (0)