diff --git a/src/CONST.js b/src/CONST.js
index 6a4d707d4ff5..63c74d1f0598 100644
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -81,6 +81,14 @@ const CONST = {
TIMEZONE: 'timeZone',
},
DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'},
+ PRONOUNS: {
+ THEY_THEM_THEIRS: 'They/them/theirs',
+ SHE_HER_HERS: 'She/her/hers',
+ HE_HIM_HIS: 'He/him/his',
+ ZE_HIR_HIRS: 'Ze/hir/hirs',
+ SELF_SELECT: 'Self-select',
+ CALL_ME_BY_MY_NAME: 'Call me by my name',
+ },
APP_STATE: {
ACTIVE: 'active',
BACKGROUND: 'background',
diff --git a/src/components/Checkbox.js b/src/components/Checkbox.js
new file mode 100644
index 000000000000..674240f3f2e6
--- /dev/null
+++ b/src/components/Checkbox.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import {View, Pressable, Text} from 'react-native';
+import PropTypes from 'prop-types';
+import styles from '../styles/styles';
+import Icon from './Icon';
+import {Checkmark} from './Icon/Expensicons';
+
+const propTypes = {
+ // Whether checkbox is checked
+ isChecked: PropTypes.bool.isRequired,
+
+ // A function that is called when the box/label is clicked on
+ onCheckboxClick: PropTypes.func.isRequired,
+
+ // Text that appears next to check box
+ label: PropTypes.string,
+};
+
+const defaultProps = {
+ label: '',
+};
+
+const Checkbox = ({
+ isChecked,
+ onCheckboxClick,
+ label,
+}) => (
+
+ onCheckboxClick(!isChecked)}>
+
+
+
+
+ {label && (
+ onCheckboxClick(!isChecked)}>
+
+ {label}
+
+
+ )}
+
+);
+
+Checkbox.defaultProps = defaultProps;
+Checkbox.propTypes = propTypes;
+Checkbox.displayName = 'Checkbox';
+
+export default Checkbox;
diff --git a/src/components/OptionsSelector.js b/src/components/OptionsSelector.js
index 48077e0a9d93..8e976eab31c7 100644
--- a/src/components/OptionsSelector.js
+++ b/src/components/OptionsSelector.js
@@ -179,7 +179,7 @@ class OptionsSelector extends Component {
onChangeText={this.props.onChangeText}
onKeyPress={this.handleKeyPress}
placeholder={this.props.placeholderText}
- placeholderTextColor={themeColors.textSupporting}
+ placeholderTextColor={themeColors.placeholderText}
/>
this.textInput = el}
textAlignVertical="top"
placeholder="Write something..."
- placeholderTextColor={themeColors.textSupporting}
+ placeholderTextColor={themeColors.placeholderText}
onChangeText={this.updateComment}
onKeyPress={this.triggerSubmitShortcut}
onDragEnter={() => this.setState({isDraggingOver: true})}
diff --git a/src/pages/settings/ProfilePage.js b/src/pages/settings/ProfilePage.js
index 93e284f709fc..1939a775de77 100644
--- a/src/pages/settings/ProfilePage.js
+++ b/src/pages/settings/ProfilePage.js
@@ -1,20 +1,276 @@
-import React from 'react';
+import React, {Component} from 'react';
+import {withOnyx} from 'react-native-onyx';
+import PropTypes from 'prop-types';
+import {
+ View,
+ TextInput,
+ Pressable,
+} from 'react-native';
+import RNPickerSelect from 'react-native-picker-select';
+import Str from 'expensify-common/lib/str';
+import moment from 'moment-timezone';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
import Navigation from '../../libs/Navigation/Navigation';
import ScreenWrapper from '../../components/ScreenWrapper';
+import {setPersonalDetails} from '../../libs/actions/PersonalDetails';
import ROUTES from '../../ROUTES';
+import ONYXKEYS from '../../ONYXKEYS';
+import CONST from '../../CONST';
+import Avatar from '../../components/Avatar';
+import styles from '../../styles/styles';
+import Text from '../../components/Text';
+import {DownArrow} from '../../components/Icon/Expensicons';
+import Icon from '../../components/Icon';
+import Checkbox from '../../components/Checkbox';
+import themeColors from '../../styles/themes/default';
-const ProfilePage = () => (
-
- Navigation.navigate(ROUTES.SETTINGS)}
- onCloseButtonPress={() => Navigation.dismissModal()}
- />
-
-);
+const propTypes = {
+ /* Onyx Props */
+ // The personal details of the person who is logged in
+ myPersonalDetails: PropTypes.shape({
+ // Email/Phone login of the current user from their personal details
+ login: PropTypes.string,
+ // Display first name of the current user from their personal details
+ firstName: PropTypes.string,
+
+ // Display last name of the current user from their personal details
+ lastName: PropTypes.string,
+
+ // Avatar URL of the current user from their personal details
+ avatar: PropTypes.string,
+
+ // Pronouns of the current user from their personal details
+ pronouns: PropTypes.string,
+
+ // timezone of the current user from their personal details
+ timezone: PropTypes.shape({
+
+ // Value of selected timezone
+ selected: PropTypes.string,
+
+ // Whether timezone is automatically set
+ automatic: PropTypes.bool,
+ }),
+ }),
+};
+
+const defaultProps = {
+ myPersonalDetails: {},
+};
+
+const timezones = moment.tz.names()
+ .map(timezone => ({
+ value: timezone,
+ label: timezone,
+ }));
+
+class ProfilePage extends Component {
+ constructor(props) {
+ super(props);
+
+ const {
+ firstName,
+ lastName,
+ pronouns,
+ timezone = {},
+ } = props.myPersonalDetails;
+ const pronounsList = Object.values(CONST.PRONOUNS);
+
+ let currentUserPronouns = pronouns;
+ let initialSelfSelectedPronouns = '';
+
+ // This handles populating the self-selected pronouns in the form
+ if (pronouns && !pronounsList.includes(pronouns)) {
+ currentUserPronouns = CONST.PRONOUNS.SELF_SELECT;
+ initialSelfSelectedPronouns = pronouns;
+ }
+
+ this.state = {
+ firstName,
+ lastName,
+ pronouns: currentUserPronouns,
+ selfSelectedPronouns: initialSelfSelectedPronouns,
+ selectedTimezone: timezone.selected || CONST.DEFAULT_TIME_ZONE.selected,
+ isAutomaticTimezone: timezone.automatic ?? CONST.DEFAULT_TIME_ZONE.automatic,
+ };
+
+ this.pronounDropdownValues = pronounsList.map(pronoun => ({value: pronoun, label: pronoun}));
+ this.updatePersonalDetails = this.updatePersonalDetails.bind(this);
+ this.setAutomaticTimezone = this.setAutomaticTimezone.bind(this);
+ }
+
+ setAutomaticTimezone(isAutomaticTimezone) {
+ this.setState(({selectedTimezone}) => ({
+ isAutomaticTimezone,
+ selectedTimezone: isAutomaticTimezone ? moment.tz.guess() : selectedTimezone,
+ }));
+ }
+
+ updatePersonalDetails() {
+ const {
+ firstName,
+ lastName,
+ pronouns,
+ selfSelectedPronouns,
+ selectedTimezone,
+ isAutomaticTimezone,
+ } = this.state;
+
+ setPersonalDetails({
+ firstName,
+ lastName,
+ pronouns: pronouns === CONST.PRONOUNS.SELF_SELECT ? selfSelectedPronouns : pronouns,
+ timezone: {
+ automatic: isAutomaticTimezone,
+ selected: selectedTimezone,
+ },
+ });
+ }
+
+ render() {
+ // Determines if the pronouns/selected pronouns have changed
+ const arePronounsUnchanged = this.props.myPersonalDetails.pronouns === this.state.pronouns
+ || (this.props.myPersonalDetails.pronouns
+ && this.props.myPersonalDetails.pronouns === this.state.selfSelectedPronouns);
+
+ // Disables button if none of the form values have changed
+ const isButtonDisabled = (this.props.myPersonalDetails.firstName === this.state.firstName)
+ && (this.props.myPersonalDetails.lastName === this.state.lastName)
+ && (this.props.myPersonalDetails.timezone.selected === this.state.selectedTimezone)
+ && (this.props.myPersonalDetails.timezone.automatic === this.state.isAutomaticTimezone)
+ && arePronounsUnchanged;
+
+ return (
+
+ Navigation.navigate(ROUTES.SETTINGS)}
+ onCloseButtonPress={Navigation.dismissModal}
+ />
+
+
+
+ Tell us about yourself, we would love to get to know you!
+
+
+
+ First Name
+ this.setState({firstName})}
+ placeholder="John"
+ placeholderTextColor={themeColors.placeholderText}
+ />
+
+
+ Last Name
+ this.setState({lastName})}
+ placeholder="Doe"
+ placeholderTextColor={themeColors.placeholderText}
+ />
+
+
+
+ Preferred Pronouns
+
+ this.setState({pronouns, selfSelectedPronouns: ''})}
+ items={this.pronounDropdownValues}
+ style={styles.picker}
+ useNativeAndroidPickerStyle={false}
+ placeholder={{
+ value: '',
+ label: 'Select your pronouns',
+ }}
+ value={this.state.pronouns}
+ Icon={() => }
+ />
+
+ {this.state.pronouns === CONST.PRONOUNS.SELF_SELECT && (
+ this.setState({selfSelectedPronouns})}
+ placeholder="Self-select your pronoun"
+ placeholderTextColor={themeColors.placeholderText}
+ />
+ )}
+
+
+
+ {Str.isSMSLogin(this.props.myPersonalDetails.login)
+ ? 'Phone Number' : 'Email Address'}
+
+
+
+
+ Timezone
+ this.setState({selectedTimezone})}
+ items={timezones}
+ style={this.state.isAutomaticTimezone ? {
+ ...styles.picker,
+ inputIOS: [styles.picker.inputIOS, styles.textInput, styles.disabledTextInput],
+ inputAndroid: [
+ styles.picker.inputAndroid, styles.textInput, styles.disabledTextInput,
+ ],
+ inputWeb: [styles.picker.inputWeb, styles.textInput, styles.disabledTextInput],
+ } : styles.picker}
+ useNativeAndroidPickerStyle={false}
+ value={this.state.selectedTimezone}
+ Icon={() => }
+ disabled={this.state.isAutomaticTimezone}
+ />
+
+
+
+
+ [
+ styles.button,
+ styles.buttonSuccess,
+ styles.w100,
+ hovered && styles.buttonSuccessHovered,
+ isButtonDisabled && styles.buttonDisable,
+ ]}
+ >
+
+ Save
+
+
+
+
+ );
+ }
+}
+
+ProfilePage.propTypes = propTypes;
+ProfilePage.defaultProps = defaultProps;
ProfilePage.displayName = 'ProfilePage';
-export default ProfilePage;
+export default withOnyx({
+ myPersonalDetails: {
+ key: ONYXKEYS.MY_PERSONAL_DETAILS,
+ },
+})(ProfilePage);
diff --git a/src/pages/signin/LoginForm/LoginFormNarrow.js b/src/pages/signin/LoginForm/LoginFormNarrow.js
index 08ecb0aee47d..50b5c72341bd 100644
--- a/src/pages/signin/LoginForm/LoginFormNarrow.js
+++ b/src/pages/signin/LoginForm/LoginFormNarrow.js
@@ -72,7 +72,7 @@ class LoginFormNarrow extends React.Component {
onSubmitEditing={this.validateAndSubmitForm}
autoCapitalize="none"
placeholder="Phone or Email"
- placeholderTextColor={themeColors.textSupporting}
+ placeholderTextColor={themeColors.placeholderText}
/>
diff --git a/src/pages/signin/PasswordForm.js b/src/pages/signin/PasswordForm.js
index 68aca6d584ae..8c9b95b9f284 100644
--- a/src/pages/signin/PasswordForm.js
+++ b/src/pages/signin/PasswordForm.js
@@ -85,7 +85,7 @@ class PasswordForm extends React.Component {
style={[styles.textInput]}
value={this.state.twoFactorAuthCode}
placeholder="Required when 2FA is enabled"
- placeholderTextColor={themeColors.textSupporting}
+ placeholderTextColor={themeColors.placeholderText}
onChangeText={text => this.setState({twoFactorAuthCode: text})}
onSubmitEditing={this.validateAndSubmitForm}
keyboardType="numeric"
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 4baa4294d3d7..d8c18e0b03f6 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -178,36 +178,45 @@ const styles = {
inputIOS: {
fontFamily: fontFamily.GTA,
fontSize: variables.fontSizeNormal,
- paddingVertical: 12,
- paddingHorizontal: 10,
+ paddingLeft: 12,
+ paddingRight: 12,
+ paddingTop: 10,
+ paddingBottom: 10,
borderRadius: variables.componentBorderRadius,
borderWidth: 1,
borderColor: themeColors.border,
color: themeColors.text,
- paddingRight: 30,
+ height: variables.componentSizeNormal,
+ opacity: 1,
},
inputWeb: {
fontFamily: fontFamily.GTA,
fontSize: variables.fontSizeNormal,
- paddingVertical: 12,
- paddingHorizontal: 10,
+ paddingLeft: 12,
+ paddingRight: 12,
+ paddingTop: 10,
+ paddingBottom: 10,
borderWidth: 1,
borderRadius: variables.componentBorderRadius,
borderColor: themeColors.border,
color: themeColors.text,
- paddingRight: 30,
appearance: 'none',
+ height: variables.componentSizeNormal,
+ opacity: 1,
},
inputAndroid: {
fontFamily: fontFamily.GTA,
fontSize: variables.fontSizeNormal,
- paddingHorizontal: 10,
- paddingVertical: 8,
+ paddingLeft: 12,
+ paddingRight: 12,
+ paddingTop: 10,
+ paddingBottom: 10,
borderWidth: 1,
borderRadius: variables.componentBorderRadius,
borderColor: themeColors.border,
color: themeColors.text,
- paddingRight: 30,
+ height: variables.componentSizeNormal,
+ opacity: 1,
},
iconContainer: {
top: 12,
@@ -290,6 +299,11 @@ const styles = {
textAlignVertical: 'center',
},
+ disabledTextInput: {
+ backgroundColor: colors.gray1,
+ color: colors.gray3,
+ },
+
textInputReversed: addOutlineWidth({
backgroundColor: themeColors.heading,
borderColor: themeColors.text,
@@ -1113,6 +1127,21 @@ const styles = {
lineHeight: 20,
},
+ checkboxContainer: {
+ backgroundColor: themeColors.componentBG,
+ borderRadius: 2,
+ height: 20,
+ width: 20,
+ borderColor: themeColors.border,
+ borderWidth: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+
+ checkedContainer: {
+ backgroundColor: colors.blue,
+ },
+
iouAmountText: {
fontFamily: fontFamily.GTA_BOLD,
fontWeight: fontWeightBold,
diff --git a/src/styles/themes/default.js b/src/styles/themes/default.js
index 3c4ba846a6e7..73b82ded23ab 100644
--- a/src/styles/themes/default.js
+++ b/src/styles/themes/default.js
@@ -34,4 +34,5 @@ export default {
buttonHoveredBG: colors.gray1,
spinner: colors.gray4,
unreadIndicator: colors.green,
+ placeholderText: colors.gray3,
};
diff --git a/src/styles/utilities/flex.js b/src/styles/utilities/flex.js
index 1508fbba8c61..6a66532e0601 100644
--- a/src/styles/utilities/flex.js
+++ b/src/styles/utilities/flex.js
@@ -40,6 +40,10 @@ export default {
alignSelf: 'stretch',
},
+ alignSelfCenter: {
+ alignSelf: 'center',
+ },
+
alignItemsCenter: {
alignItems: 'center',
},