-
-
Notifications
You must be signed in to change notification settings - Fork 344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(1) feat: Add Feedback Form Component #4328
Changes from 89 commits
817eac8
5370a99
42e2fa1
514b102
0935bbd
9ea5496
da0d4ac
3e36c6d
5f3df64
71b28e8
f9d2b59
d05d531
2bb104b
4339274
6ce799b
f2cefc6
694ee33
18b1c33
034efde
67a492d
8eaa61d
0b88cc5
ae11b8d
7f2ca06
064b6c4
e2add4a
e21718a
407f179
14ac005
ddade00
1bc1e4c
7934756
95e1e0f
a169362
4b5df7a
4fa81ce
4fff82f
0258bf2
458ebc2
f0e1bef
b9235f2
6717a84
39a67bd
501a134
4b290a2
7109deb
c80c5cb
8c56753
d6e9229
5292475
efd809f
bc7ae65
79ee5ba
ba13320
1f5fb56
439367a
9831482
4097347
f8a82fd
30a7b10
3eccf25
bc96fce
995a9ca
3aacaf7
20e3a6c
78e412c
c45a5e6
6e39119
57d99e9
6fb8ab4
fd2e317
b793937
b209ad6
a40fde6
2a8f13f
0f3a244
9a96e74
10c1c0e
2be08a6
0588552
e0ec92d
265e629
da0e3ea
a931ba9
5f9dec6
8359696
76518cc
b0384ed
61886c2
9853630
e7a9484
06b99c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import type { FeedbackFormStyles } from './FeedbackForm.types'; | ||
|
||
const PURPLE = 'rgba(88, 74, 192, 1)'; | ||
const FORGROUND_COLOR = '#2b2233'; | ||
const BACKROUND_COLOR = '#ffffff'; | ||
const BORDER_COLOR = 'rgba(41, 35, 47, 0.13)'; | ||
|
||
const defaultStyles: FeedbackFormStyles = { | ||
krystofwoldrich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
container: { | ||
flex: 1, | ||
padding: 20, | ||
backgroundColor: BACKROUND_COLOR, | ||
}, | ||
title: { | ||
fontSize: 24, | ||
fontWeight: 'bold', | ||
marginBottom: 20, | ||
textAlign: 'left', | ||
flex: 1, | ||
color: FORGROUND_COLOR, | ||
}, | ||
label: { | ||
marginBottom: 4, | ||
fontSize: 16, | ||
color: FORGROUND_COLOR, | ||
}, | ||
input: { | ||
height: 50, | ||
borderColor: BORDER_COLOR, | ||
borderWidth: 1, | ||
borderRadius: 5, | ||
paddingHorizontal: 10, | ||
marginBottom: 15, | ||
fontSize: 16, | ||
color: FORGROUND_COLOR, | ||
}, | ||
textArea: { | ||
height: 100, | ||
textAlignVertical: 'top', | ||
color: FORGROUND_COLOR, | ||
}, | ||
submitButton: { | ||
backgroundColor: PURPLE, | ||
paddingVertical: 15, | ||
borderRadius: 5, | ||
alignItems: 'center', | ||
marginBottom: 10, | ||
}, | ||
submitText: { | ||
color: BACKROUND_COLOR, | ||
fontSize: 18, | ||
}, | ||
cancelButton: { | ||
paddingVertical: 15, | ||
alignItems: 'center', | ||
}, | ||
cancelText: { | ||
color: FORGROUND_COLOR, | ||
fontSize: 16, | ||
}, | ||
titleContainer: { | ||
flexDirection: 'row', | ||
width: '100%', | ||
}, | ||
sentryLogo: { | ||
width: 40, | ||
height: 40, | ||
}, | ||
}; | ||
|
||
export default defaultStyles; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
import type { SendFeedbackParams } from '@sentry/core'; | ||
import { captureFeedback, getCurrentScope, lastEventId, logger } from '@sentry/core'; | ||
import * as React from 'react'; | ||
import type { KeyboardTypeOptions } from 'react-native'; | ||
import { | ||
Alert, | ||
Image, | ||
Keyboard, | ||
KeyboardAvoidingView, | ||
SafeAreaView, | ||
ScrollView, | ||
Text, | ||
TextInput, | ||
TouchableOpacity, | ||
TouchableWithoutFeedback, | ||
View | ||
} from 'react-native'; | ||
|
||
import { sentryLogo } from './branding'; | ||
import { defaultConfiguration } from './defaults'; | ||
import defaultStyles from './FeedbackForm.styles'; | ||
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles,FeedbackGeneralConfiguration, FeedbackTextConfiguration } from './FeedbackForm.types'; | ||
|
||
let feedbackFormHandler: (() => void) | null = null; | ||
|
||
const setFeedbackFormHandler = (handler: () => void): void => { | ||
feedbackFormHandler = handler; | ||
}; | ||
|
||
const clearFeedbackFormHandler = (): void => { | ||
feedbackFormHandler = null; | ||
}; | ||
|
||
type Navigation = { | ||
navigate: (screen: string, params?: Record<string, unknown>) => void; | ||
}; | ||
|
||
export const showFeedbackForm = (navigation: Navigation): void => { | ||
setFeedbackFormHandler(() => { | ||
navigation?.navigate?.('FeedbackForm'); | ||
}); | ||
if (feedbackFormHandler) { | ||
feedbackFormHandler(); | ||
} else { | ||
logger.error('FeedbackForm handler is not set. Please ensure it is initialized.'); | ||
} | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was merged from #4370 I would keep the auto inject out of the this PR. I think we should not export this helper, because to use it user still have to create a screen which will present the The auto inject should work without manually specifying the FeedbackForm component and should be independent of any nav library. (We can create helper for specific bav libs later...) I imagine when I call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense @krystofwoldrich 👍 |
||
|
||
/** | ||
* @beta | ||
* Implements a feedback form screen that sends feedback to Sentry using Sentry.captureFeedback. | ||
*/ | ||
export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFormState> { | ||
public static defaultProps: Partial<FeedbackFormProps> = { | ||
...defaultConfiguration | ||
} | ||
|
||
public constructor(props: FeedbackFormProps) { | ||
super(props); | ||
|
||
const currentUser = { | ||
useSentryUser: { | ||
email: this.props?.useSentryUser?.email || getCurrentScope()?.getUser()?.email || '', | ||
name: this.props?.useSentryUser?.name || getCurrentScope()?.getUser()?.name || '', | ||
} | ||
} | ||
|
||
this.state = { | ||
isVisible: true, | ||
name: currentUser.useSentryUser.name, | ||
email: currentUser.useSentryUser.email, | ||
description: '', | ||
}; | ||
} | ||
|
||
/** | ||
* Clear the handler when the component unmounts | ||
*/ | ||
public componentWillUnmount(): void { | ||
clearFeedbackFormHandler(); | ||
} | ||
|
||
public handleFeedbackSubmit: () => void = () => { | ||
const { name, email, description } = this.state; | ||
const { onFormClose } = this.props; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is addressed in https://github.com/getsentry/sentry-react-native/pull/4364/files |
||
const text: FeedbackTextConfiguration = this.props; | ||
|
||
const trimmedName = name?.trim(); | ||
const trimmedEmail = email?.trim(); | ||
const trimmedDescription = description?.trim(); | ||
|
||
if ((this.props.isNameRequired && !trimmedName) || (this.props.isEmailRequired && !trimmedEmail) || !trimmedDescription) { | ||
Alert.alert(text.errorTitle, text.formError); | ||
return; | ||
} | ||
|
||
if (this.props.shouldValidateEmail && (this.props.isEmailRequired || trimmedEmail.length > 0) && !this._isValidEmail(trimmedEmail)) { | ||
Alert.alert(text.errorTitle, text.emailError); | ||
return; | ||
} | ||
|
||
const eventId = lastEventId(); | ||
const userFeedback: SendFeedbackParams = { | ||
message: trimmedDescription, | ||
name: trimmedName, | ||
email: trimmedEmail, | ||
associatedEventId: eventId, | ||
}; | ||
|
||
onFormClose(); | ||
this.setState({ isVisible: false }); | ||
|
||
captureFeedback(userFeedback); | ||
Alert.alert(text.successMessageText); | ||
}; | ||
|
||
/** | ||
* Renders the feedback form screen. | ||
*/ | ||
public render(): React.ReactNode { | ||
const { name, email, description } = this.state; | ||
const { onFormClose } = this.props; | ||
const config: FeedbackGeneralConfiguration = this.props; | ||
const text: FeedbackTextConfiguration = this.props; | ||
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles }; | ||
const onCancel = (): void => { | ||
onFormClose(); | ||
this.setState({ isVisible: false }); | ||
} | ||
|
||
if (!this.state.isVisible) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<SafeAreaView style={[styles.container, { padding: 0 }]}> | ||
<KeyboardAvoidingView behavior={'padding'} style={[styles.container, { padding: 0 }]}> | ||
<ScrollView> | ||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}> | ||
<View style={styles.container}> | ||
<View style={styles.titleContainer}> | ||
<Text style={styles.title}>{text.formTitle}</Text> | ||
{config.showBranding && ( | ||
<Image | ||
source={{ uri: sentryLogo }} | ||
style={styles.sentryLogo} | ||
testID='sentry-logo' | ||
/> | ||
)} | ||
</View> | ||
|
||
{config.showName && ( | ||
<> | ||
<Text style={styles.label}> | ||
{text.nameLabel} | ||
{config.isNameRequired && ` ${text.isRequiredLabel}`} | ||
</Text> | ||
<TextInput | ||
style={styles.input} | ||
placeholder={text.namePlaceholder} | ||
value={name} | ||
onChangeText={(value) => this.setState({ name: value })} | ||
/> | ||
</> | ||
)} | ||
|
||
{config.showEmail && ( | ||
<> | ||
<Text style={styles.label}> | ||
{text.emailLabel} | ||
{config.isEmailRequired && ` ${text.isRequiredLabel}`} | ||
</Text> | ||
<TextInput | ||
style={styles.input} | ||
placeholder={text.emailPlaceholder} | ||
keyboardType={'email-address' as KeyboardTypeOptions} | ||
value={email} | ||
onChangeText={(value) => this.setState({ email: value })} | ||
/> | ||
</> | ||
)} | ||
|
||
<Text style={styles.label}> | ||
{text.messageLabel} | ||
{` ${text.isRequiredLabel}`} | ||
</Text> | ||
<TextInput | ||
style={[styles.input, styles.textArea]} | ||
placeholder={text.messagePlaceholder} | ||
value={description} | ||
onChangeText={(value) => this.setState({ description: value })} | ||
multiline | ||
/> | ||
|
||
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}> | ||
<Text style={styles.submitText}>{text.submitButtonLabel}</Text> | ||
</TouchableOpacity> | ||
|
||
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}> | ||
<Text style={styles.cancelText}>{text.cancelButtonLabel}</Text> | ||
</TouchableOpacity> | ||
</View> | ||
</TouchableWithoutFeedback> | ||
</ScrollView> | ||
</KeyboardAvoidingView> | ||
</SafeAreaView> | ||
); | ||
} | ||
|
||
private _isValidEmail = (email: string): boolean => { | ||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ | ||
return emailRegex.test(email); | ||
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved
Hide resolved
|
||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed with 9853630