Skip to content

Commit

Permalink
Merge pull request #181 from appfolio/feature/credit-card-number-field
Browse files Browse the repository at this point in the history
Credit Card Validation Components
  • Loading branch information
gthomas-appfolio authored Apr 13, 2017
2 parents 8f35ba6 + c039743 commit c059d65
Show file tree
Hide file tree
Showing 17 changed files with 465 additions and 6,175 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ coverage
node_modules
dist
*.log
yarn.lock
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
"babel-preset-es2015": "~6.14.0",
"babel-preset-react": "~6.11.1",
"card-validator": "^4.0.0",
"css-loader": "~0.25.0",
"credit-card-type": "^5.0.1",
"css-loader": "~0.25.0",
"enzyme": "^2.6.0",
"eslint": "^3.17.1",
"eslint-config-appfolio-react": "~0.3.2",
Expand Down
42 changes: 18 additions & 24 deletions src/components/AddressInput.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { FormGroup, Input, Row, Col, Select, FormFeedback } from '../';
import { Col, Input, Row, Select, ValidatedFormGroup } from '../';
import flow from 'lodash.flow';
import noop from 'lodash.noop';

Expand Down Expand Up @@ -31,7 +31,7 @@ class AddressInput extends Component {

return (
<div>
<FormGroup color={error.address1 && 'danger'}>
<ValidatedFormGroup error={error.address1}>
<Input
name="address1"
type="text"
Expand All @@ -41,9 +41,8 @@ class AddressInput extends Component {
onChange={flow([readEvent, this.onChange])}
disabled={disabled}
/>
{error.address1 && <FormFeedback children={error.address1} />}
</FormGroup>
<FormGroup color={error.address2 && 'danger'}>
</ValidatedFormGroup>
<ValidatedFormGroup error={error.address2}>
<Input
name="address2"
type="text"
Expand All @@ -53,11 +52,10 @@ class AddressInput extends Component {
onChange={flow([readEvent, this.onChange])}
disabled={disabled}
/>
{error.address2 && <FormFeedback children={error.address2} />}
</FormGroup>
</ValidatedFormGroup>
<Row className="no-gutters">
<Col sm={6} xs={5}>
<FormGroup className="pr-3" color={error.city && 'danger'}>
<ValidatedFormGroup className="pr-3" error={error.city}>
<Input
type="text"
name="city"
Expand All @@ -67,11 +65,10 @@ class AddressInput extends Component {
onChange={flow([readEvent, this.onChange])}
disabled={disabled}
/>
{error.city && <FormFeedback children={error.city} />}
</FormGroup>
</ValidatedFormGroup>
</Col>
<Col sm={2} xs={3}>
<FormGroup className="pr-3" color={error.state && 'danger'}>
<ValidatedFormGroup className="pr-3" error={error.state}>
<Select
className="w-100"
name="state"
Expand All @@ -81,11 +78,10 @@ class AddressInput extends Component {
onChange={selection => this.onChange({ state: selection && selection.value })}
disabled={disabled}
/>
{error.state && <FormFeedback children={error.state} />}
</FormGroup>
</ValidatedFormGroup>
</Col>
<Col sm={4} xs={4}>
<FormGroup color={error.postal && 'danger'}>
<ValidatedFormGroup error={error.postal}>
<Input
type="text"
name="postal"
Expand All @@ -95,11 +91,10 @@ class AddressInput extends Component {
onChange={flow([readEvent, this.onChange])}
disabled={disabled}
/>
{error.postal && <FormFeedback children={error.postal} />}
</FormGroup>
</ValidatedFormGroup>
</Col>
</Row>
<FormGroup color={error.countryCode && 'danger'}>
<ValidatedFormGroup error={error.countryCode}>
<Select
className="w-100"
name="countryCode"
Expand All @@ -109,26 +104,25 @@ class AddressInput extends Component {
onChange={selection => this.onChange({ countryCode: selection && selection.value })}
disabled={disabled}
/>
{error.countryCode && <FormFeedback children={error.countryCode} />}
</FormGroup>
</ValidatedFormGroup>
</div>
);
}
}

const fieldTypes = {
export const addressPropType = {
address1: React.PropTypes.string,
address2: React.PropTypes.string,
city: React.PropTypes.string,
state: React.PropTypes.string,
postal: React.PropTypes.string,
countryCode: React.PropTypes.string
countryCode: React.PropTypes.string,
};

AddressInput.propTypes = {
value: React.PropTypes.shape(fieldTypes),
defaultValue: React.PropTypes.shape(fieldTypes),
error: React.PropTypes.shape(fieldTypes),
value: React.PropTypes.shape(addressPropType),
defaultValue: React.PropTypes.shape(addressPropType),
error: React.PropTypes.shape(addressPropType),
onChange: React.PropTypes.func,
disabled: React.PropTypes.bool
};
Expand Down
69 changes: 69 additions & 0 deletions src/components/CreditCardExpiration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { Component, PropTypes } from 'react';
import { Col, Row } from 'reactstrap';
import { Select } from '../';

const today = new Date();
const MONTHS = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
];
const YEARS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.map(offset => today.getFullYear() + offset);

// eslint-disable-next-line arrow-body-style
const monthOptions = MONTHS.map((label, index) => ({ label, value: index + 1 }));
// eslint-disable-next-line arrow-body-style
const yearsOptions = YEARS.map(year => ({ label: year, value: year }));

export default class CreditCardExpiration extends Component {
onMonthSelection = (option) => {
const month = option && option.value || CreditCardExpiration.defaultProps.month;
this.props.onChange({ month, year: this.props.year });
}
onYearSelection = (option) => {
const year = option && option.value || CreditCardExpiration.defaultProps.year;
this.props.onChange({ year, month: this.props.month });
}

render() {
return (
<Row className="no-gutters">
<Col xs={7} sm={8}>
<Select
name={this.props.monthName}
placeholder="Month"
value={this.props.month}
options={monthOptions}
onChange={this.onMonthSelection}
className="pr-3"
/>
</Col>
<Col xs={5} sm={4}>
<Select
name={this.props.yearName}
placeholder="Year" value={this.props.year}
options={yearsOptions} onChange={this.onYearSelection}
/>
</Col>
</Row>
);
}
}

CreditCardExpiration.defaultProps = {
month: null,
monthName: 'month',
year: null,
yearName: 'year',

onChange: () => true,
};

CreditCardExpiration.propTypes = {
month: PropTypes.number,
monthName: PropTypes.string,
year: PropTypes.number,
yearName: PropTypes.string,

onChange: PropTypes.func,
};
102 changes: 102 additions & 0 deletions src/components/CreditCardInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { Component, PropTypes } from 'react';
import {
Row, Col, PatternInput, ValidatedFormGroup,
CreditCardExpiration, CreditCardNumber,
} from '../';

export default class CreditCardInput extends Component {
handleCardCVVChange = (event, { value, isValid }) => {
this.props.onChange({ cardCVV: value, cardCVVIsValid: isValid });
}
handleCardExpirationChange = ({ month, year }) => {
const TODAY = new Date();
const expirationIsValid = new Date(year, month) >= TODAY;
this.props.onChange({ expirationMonth: month, expirationYear: year, expirationIsValid });
}
handleCardNumberChange = (cardNumber, cardNumberIsValid) => {
this.props.onChange({ cardNumber, cardNumberIsValid });
}

render() {
const {
cardNumber, cardCVV, errors,
expirationMonth: month, expirationYear: year,
} = this.props;

return (
<Row>
<Col xs={12} sm={6}>
<Row className="no-gutters">
<Col xs={9} sm={10}>
<ValidatedFormGroup className="pr-3" error={errors.cardNumber}>
<CreditCardNumber
value={cardNumber}
placeholder="Card Number"
onChange={this.handleCardNumberChange}
/>
</ValidatedFormGroup>
</Col>
<Col xs={3} sm={2}>
<ValidatedFormGroup error={errors.cardCVV}>
<PatternInput
name="cardCVV"
placeholder="CVV"
type="text"
value={cardCVV}
pattern={/^[0-9]{0,5}$/}
onChange={this.handleCardCVVChange}
/>
</ValidatedFormGroup>
</Col>
</Row>
</Col>
<Col xs={12} sm={6}>
<ValidatedFormGroup error={errors.expiration}>
<CreditCardExpiration
month={month}
monthName="expiration"
year={year}
yearName="expiration"
onChange={this.handleCardExpirationChange}
/>
</ValidatedFormGroup>
</Col>
</Row>
);
}
}

export const creditCardPropTypes = {
cardNumber: PropTypes.string,
cardCVV: PropTypes.string,
errors: PropTypes.object,

expirationMonth: (props, propName) => {
const value = props[propName];
if (value === null) return null;
return typeof value === 'number' && value >= 1 && value <= 12
? null : new Error('Expiration Month must be a number between 1 and 12');
},
expirationYear: (props, propName) => {
const value = props[propName];
if (value === null) return null;

const TODAY = new Date();
return typeof value === 'number' && value >= TODAY.getFullYear()
? null : new Error('Expiration Year must be this year or later');
},

onChange: PropTypes.func,
};


CreditCardInput.propTypes = creditCardPropTypes;
CreditCardInput.defaultProps = {
cardNumber: '',
cardCVV: '',
errors: {},
expirationMonth: null,
expirationYear: null,

onChange: () => {},
};
Loading

0 comments on commit c059d65

Please sign in to comment.