diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index e3ec98b6052e..9e198d07aad7 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -155,9 +155,6 @@ export default { // Set when we are loading payment methods IS_LOADING_PAYMENT_METHODS: 'isLoadingPaymentMethods', - // Stores values for the add debit card form - ADD_DEBIT_CARD_FORM: 'addDebitCardForm', - // Stores values for the request call form REQUEST_CALL_FORM: 'requestCallForm', @@ -193,4 +190,9 @@ export default { // Validating Email? USER_SIGN_UP: 'userSignUp', + + // List of Form ids + FORMS: { + ADD_DEBIT_CARD_FORM: 'addDebitCardForm', + }, }; diff --git a/src/components/AddressSearch.js b/src/components/AddressSearch.js index 7ad5474ae06b..fefddffa0315 100644 --- a/src/components/AddressSearch.js +++ b/src/components/AddressSearch.js @@ -3,6 +3,7 @@ import React, {useState} from 'react'; import PropTypes from 'prop-types'; import {LogBox, ScrollView, View} from 'react-native'; import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete'; +import lodashGet from 'lodash/get'; import CONFIG from '../CONFIG'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import styles from '../styles/styles'; @@ -46,6 +47,14 @@ const propTypes = { /** Customize the TextInput container */ containerStyles: PropTypes.arrayOf(PropTypes.object), + /** A map of inputID key names */ + renamedInputKeys: PropTypes.shape({ + street: PropTypes.string, + city: PropTypes.string, + state: PropTypes.string, + zipCode: PropTypes.string, + }), + ...withLocalizePropTypes, }; @@ -58,6 +67,12 @@ const defaultProps = { value: undefined, defaultValue: undefined, containerStyles: [], + renamedInputKeys: { + street: 'addressStreet', + city: 'addressCity', + state: 'addressState', + zipCode: 'addressZipCode', + }, }; // Do not convert to class component! It's been tried before and presents more challenges than it's worth. @@ -107,7 +122,10 @@ const AddressSearch = (props) => { return; } if (props.inputID) { - _.each(values, (value, key) => props.onInputChange(value, key)); + _.each(values, (value, key) => { + const inputKey = lodashGet(props.renamedInputKeys, key, key); + props.onInputChange(value, inputKey); + }); } else { props.onInputChange(values); } diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index a4125b73c818..6933b28fae80 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -64,20 +64,21 @@ class BaseTextInput extends Component { componentDidUpdate() { // Activate or deactivate the label when value is changed programmatically from outside // Only update when value prop is provided - if (_.isUndefined(this.props.value) || this.state.value === this.props.value) { + const inputValue = this.props.value || this.input.value; + if (_.isEmpty(inputValue) || this.state.value === inputValue) { return; } // eslint-disable-next-line react/no-did-update-set-state - this.setState({value: this.props.value}); - this.input.setNativeProps({text: this.props.value}); + this.setState({value: inputValue}); + this.input.setNativeProps({text: inputValue}); // In some cases, When the value prop is empty, it is not properly updated on the TextInput due to its uncontrolled nature, thus manually clearing the TextInput. - if (this.props.value === '') { + if (inputValue === '') { this.input.clear(); } - if (this.props.value) { + if (inputValue) { this.activateLabel(); } else if (!this.state.isFocused) { this.deactivateLabel(); diff --git a/src/libs/actions/PaymentMethods.js b/src/libs/actions/PaymentMethods.js index 4fa0da7d5369..af54f858a40a 100644 --- a/src/libs/actions/PaymentMethods.js +++ b/src/libs/actions/PaymentMethods.js @@ -140,7 +140,6 @@ function addBillingCard(params) { const cardMonth = CardUtils.getMonthFromExpirationDateString(params.expirationDate); const cardYear = CardUtils.getYearFromExpirationDateString(params.expirationDate); - Onyx.merge(ONYXKEYS.ADD_DEBIT_CARD_FORM, {submitting: true}); DeprecatedAPI.AddBillingCard({ cardNumber: params.cardNumber, cardYear, @@ -152,7 +151,7 @@ function addBillingCard(params) { isP2PDebitCard: true, password: params.password, }).then(((response) => { - let errorMessage = ''; + let serverErrorMessage = ''; if (response.jsonCode === 200) { const cardObject = { additionalData: { @@ -173,12 +172,12 @@ function addBillingCard(params) { Growl.show(Localize.translateLocal('addDebitCardPage.growlMessageOnSave'), CONST.GROWL.SUCCESS, 3000); continueSetup(); } else { - errorMessage = response.message ? response.message : Localize.translateLocal('addDebitCardPage.error.genericFailureMessage'); + serverErrorMessage = response.message ? response.message : Localize.translateLocal('addDebitCardPage.error.genericFailureMessage'); } - Onyx.merge(ONYXKEYS.ADD_DEBIT_CARD_FORM, { - submitting: false, - error: errorMessage, + Onyx.merge(ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, { + isSubmitting: false, + serverErrorMessage, }); })); } @@ -187,9 +186,9 @@ function addBillingCard(params) { * Resets the values for the add debit card form back to their initial states */ function clearDebitCardFormErrorAndSubmit() { - Onyx.set(ONYXKEYS.ADD_DEBIT_CARD_FORM, { - submitting: false, - error: '', + Onyx.set(ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, { + isSubmitting: false, + serverErrorMessage: null, }); } diff --git a/src/pages/settings/Payments/AddDebitCardPage.js b/src/pages/settings/Payments/AddDebitCardPage.js index 7ce2c06adf32..e51e88219b89 100644 --- a/src/pages/settings/Payments/AddDebitCardPage.js +++ b/src/pages/settings/Payments/AddDebitCardPage.js @@ -1,10 +1,6 @@ import React, {Component} from 'react'; import {View} from 'react-native'; -import lodashGet from 'lodash/get'; -import lodashEndsWith from 'lodash/endsWith'; import _ from 'underscore'; -import {withOnyx} from 'react-native-onyx'; -import PropTypes from 'prop-types'; import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton'; import Navigation from '../../../libs/Navigation/Navigation'; import ScreenWrapper from '../../../components/ScreenWrapper'; @@ -19,80 +15,21 @@ import CheckboxWithLabel from '../../../components/CheckboxWithLabel'; import StatePicker from '../../../components/StatePicker'; import TextInput from '../../../components/TextInput'; import CONST from '../../../CONST'; -import FormAlertWithSubmitButton from '../../../components/FormAlertWithSubmitButton'; import ONYXKEYS from '../../../ONYXKEYS'; -import compose from '../../../libs/compose'; import AddressSearch from '../../../components/AddressSearch'; import * as ComponentUtils from '../../../libs/ComponentUtils'; -import FormScrollView from '../../../components/FormScrollView'; +import Form from '../../../components/Form'; const propTypes = { - addDebitCardForm: PropTypes.shape({ - /** Error message from API call */ - error: PropTypes.string, - - /** Whether or not the form is submitting */ - submitting: PropTypes.bool, - }), - /* Onyx Props */ ...withLocalizePropTypes, }; -const defaultProps = { - addDebitCardForm: { - error: '', - submitting: false, - }, -}; - class DebitCardPage extends Component { constructor(props) { super(props); - this.state = { - nameOnCard: '', - cardNumber: '', - expirationDate: '', - securityCode: '', - addressStreet: '', - addressState: '', - addressZipCode: '', - acceptedTerms: false, - password: '', - errors: {}, - shouldShowAlertPrompt: false, - }; - - this.requiredFields = [ - 'nameOnCard', - 'cardNumber', - 'expirationDate', - 'securityCode', - 'addressStreet', - 'addressState', - 'addressZipCode', - 'password', - 'acceptedTerms', - ]; - - // Map a field to the key of the error's translation - this.errorTranslationKeys = { - nameOnCard: 'addDebitCardPage.error.invalidName', - cardNumber: 'addDebitCardPage.error.debitCardNumber', - expirationDate: 'addDebitCardPage.error.expirationDate', - securityCode: 'addDebitCardPage.error.securityCode', - addressStreet: 'addDebitCardPage.error.addressStreet', - addressState: 'addDebitCardPage.error.addressState', - addressZipCode: 'addDebitCardPage.error.addressZipCode', - acceptedTerms: 'common.error.acceptedTerms', - password: 'addDebitCardPage.error.password', - }; - - this.submit = this.submit.bind(this); - this.clearErrorAndSetValue = this.clearErrorAndSetValue.bind(this); - this.getErrorText = this.getErrorText.bind(this); - this.addOrRemoveSlashToExpiryDate = this.addOrRemoveSlashToExpiryDate.bind(this); + this.validate = this.validate.bind(this); } /** @@ -103,116 +40,49 @@ class DebitCardPage extends Component { } /** - * @param {String} inputKey - * @returns {String} - */ - getErrorText(inputKey) { - if (!lodashGet(this.state.errors, inputKey, false)) { - return ''; - } - - return this.props.translate(this.errorTranslationKeys[inputKey]); - } - - /** + * @param {Object} values - form input values passed by the Form component * @returns {Boolean} */ - validate() { + validate(values) { const errors = {}; - if (!ValidationUtils.isValidCardName(this.state.nameOnCard)) { - errors.nameOnCard = true; - } - if (!ValidationUtils.isValidDebitCard(this.state.cardNumber.replace(/ /g, ''))) { - errors.cardNumber = true; + if (!values.nameOnCard || !ValidationUtils.isValidCardName(values.nameOnCard)) { + errors.nameOnCard = this.props.translate('addDebitCardPage.error.invalidName'); } - if (!ValidationUtils.isValidExpirationDate(this.state.expirationDate)) { - errors.expirationDate = true; + if (!values.cardNumber || !ValidationUtils.isValidDebitCard(values.cardNumber.replace(/ /g, ''))) { + errors.cardNumber = this.props.translate('addDebitCardPage.error.debitCardNumber'); } - if (!ValidationUtils.isValidSecurityCode(this.state.securityCode)) { - errors.securityCode = true; + if (!values.expirationDate || !ValidationUtils.isValidExpirationDate(values.expirationDate)) { + errors.expirationDate = this.props.translate('addDebitCardPage.error.expirationDate'); } - if (!ValidationUtils.isValidAddress(this.state.addressStreet)) { - errors.addressStreet = true; + if (!values.securityCode || !ValidationUtils.isValidSecurityCode(values.securityCode)) { + errors.securityCode = this.props.translate('addDebitCardPage.error.securityCode'); } - if (!ValidationUtils.isValidZipCode(this.state.addressZipCode)) { - errors.addressZipCode = true; + if (!values.addressStreet || !ValidationUtils.isValidAddress(values.addressStreet)) { + errors.addressStreet = this.props.translate('addDebitCardPage.error.addressStreet'); } - if (!this.state.addressState) { - errors.addressState = true; + if (!values.addressZipCode || !ValidationUtils.isValidZipCode(values.addressZipCode)) { + errors.addressZipCode = this.props.translate('addDebitCardPage.error.addressZipCode'); } - if (_.isEmpty(this.state.password.trim())) { - errors.password = true; + if (!values.addressState || !values.addressState) { + errors.addressState = this.props.translate('addDebitCardPage.error.addressState'); } - if (!this.state.acceptedTerms) { - errors.acceptedTerms = true; + if (!values.password || _.isEmpty(values.password.trim())) { + errors.password = this.props.translate('addDebitCardPage.error.password'); } - const hasErrors = _.size(errors) > 0; - this.setState({ - errors, - shouldShowAlertPrompt: hasErrors, - }); - return !hasErrors; - } - - submit() { - if (!this.validate()) { - return; + if (!values.acceptedTerms) { + errors.acceptedTerms = this.props.translate('common.error.acceptedTerms'); } - PaymentMethods.addBillingCard(this.state); - } - - /** - * Clear the error associated to inputKey if found and store the inputKey new value in the state. - * - * @param {String} inputKey - * @param {String} value - */ - clearErrorAndSetValue(inputKey, value) { - this.setState(prevState => ({ - [inputKey]: value, - errors: { - ...prevState.errors, - [inputKey]: false, - }, - })); - } - - /** - * @param {String} inputExpiryDate - */ - addOrRemoveSlashToExpiryDate(inputExpiryDate) { - this.setState((prevState) => { - let expiryDate = inputExpiryDate; - // Remove the digit and '/' when backspace is pressed with expiry date ending with '/' - if (inputExpiryDate.length < prevState.expirationDate.length - && (((inputExpiryDate.length === 3 && lodashEndsWith(inputExpiryDate, '/')) - || (inputExpiryDate.length === 2 && lodashEndsWith(prevState.expirationDate, '/'))))) { - expiryDate = inputExpiryDate.substring(0, inputExpiryDate.length - 1); - } else if (inputExpiryDate.length === 2 && _.indexOf(inputExpiryDate, '/') === -1) { - // An Expiry Date was added, so we should append a slash '/' - expiryDate = `${inputExpiryDate}/`; - } else if (inputExpiryDate.length > 2 && _.indexOf(inputExpiryDate, '/') === -1) { - // Expiry Date with MM and YY without slash, hence adding slash(/) - expiryDate = `${inputExpiryDate.slice(0, 2)}/${inputExpiryDate.slice(2)}`; - } - return { - expirationDate: expiryDate, - errors: { - ...prevState.errors, - expirationDate: false, - }, - }; - }); + return errors; } render() { @@ -225,136 +95,86 @@ class DebitCardPage extends Component { onBackButtonPress={() => Navigation.goBack()} onCloseButtonPress={() => Navigation.dismissModal(true)} /> - this.form = el} +
- - this.clearErrorAndSetValue('nameOnCard', nameOnCard)} - value={this.state.nameOnCard} - errorText={this.getErrorText('nameOnCard')} - /> - this.clearErrorAndSetValue('cardNumber', cardNumber)} - value={this.state.cardNumber} - errorText={this.getErrorText('cardNumber')} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - /> - - - - - - this.clearErrorAndSetValue('securityCode', securityCode)} - value={this.state.securityCode} - maxLength={4} - errorText={this.getErrorText('securityCode')} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - /> - + + + + + + + + + + { - const renamedFields = { - street: 'addressStreet', - state: 'addressState', - zipCode: 'addressZipCode', - }; - _.each(values, (value, inputKey) => { - if (inputKey === 'city') { - return; - } - const renamedInputKey = lodashGet(renamedFields, inputKey, inputKey); - this.clearErrorAndSetValue(renamedInputKey, value); - }); - }} - errorText={this.getErrorText('addressStreet')} /> - - - this.clearErrorAndSetValue('addressZipCode', value)} - value={this.state.addressZipCode} - errorText={this.getErrorText('addressZipCode')} - maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} - /> - - - this.clearErrorAndSetValue('addressState', value)} - value={this.state.addressState} - errorText={this.getErrorText('addressState')} - /> - - - + + + this.clearErrorAndSetValue('password', password)} - value={this.state.password} - errorText={this.getErrorText('password')} - textContentType="password" - autoCompleteType={ComponentUtils.PASSWORD_AUTOCOMPLETE_TYPE} - secureTextEntry + inputID="addressZipCode" + label={this.props.translate('common.zip')} + keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} /> - { - this.setState(prevState => ({ - acceptedTerms: !prevState.acceptedTerms, - errors: { - ...prevState.errors, - acceptedTerms: false, - }, - })); - }} - LabelComponent={() => ( - <> - {`${this.props.translate('common.iAcceptThe')}`} - - {`${this.props.translate('addDebitCardPage.expensifyTermsOfService')}`} - - - )} - style={styles.mt4} - errorText={this.getErrorText('acceptedTerms')} + + + + + + - {!_.isEmpty(this.props.addDebitCardForm.error) && ( - - - {this.props.addDebitCardForm.error} - - - )} - { - this.form.scrollTo({y: 0, animated: true}); - }} - isLoading={this.props.addDebitCardForm.submitting} + ( + <> + {`${this.props.translate('common.iAcceptThe')}`} + + {`${this.props.translate('addDebitCardPage.expensifyTermsOfService')}`} + + + )} + style={[styles.mt4]} + shouldSaveDraft /> - + ); @@ -362,13 +182,5 @@ class DebitCardPage extends Component { } DebitCardPage.propTypes = propTypes; -DebitCardPage.defaultProps = defaultProps; -export default compose( - withOnyx({ - addDebitCardForm: { - key: ONYXKEYS.ADD_DEBIT_CARD_FORM, - }, - }), - withLocalize, -)(DebitCardPage); +export default withLocalize(DebitCardPage);