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', },