Skip to content

Commit

Permalink
Merge pull request #189 from appfolio/feature/credit-card-number-repo…
Browse files Browse the repository at this point in the history
…rts-type

CreditCardNumber field should report cardType
  • Loading branch information
gthomas-appfolio authored Apr 19, 2017
2 parents 276f71f + 20491b5 commit 5c40623
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 20 deletions.
4 changes: 2 additions & 2 deletions src/components/CreditCardInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export default class CreditCardInput extends Component {
const expirationIsValid = new Date(year, month) >= TODAY;
this.props.onChange({ expirationMonth: month, expirationYear: year, expirationIsValid });
}
handleCardNumberChange = (cardNumber, cardNumberIsValid) => {
this.props.onChange({ cardNumber, cardNumberIsValid });
handleCardNumberChange = ({ cardNumber, cardType }, cardNumberIsValid) => {
this.props.onChange({ cardNumber, cardType, cardNumberIsValid });
}

render() {
Expand Down
25 changes: 14 additions & 11 deletions src/components/CreditCardNumber.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,19 @@ export default class CreditCardNumber extends Component {
setValue = (proposedValue) => {
let value = proposedValue.replace(/[^0-9]/g, '');
if (proposedValue === '') {
this.props.onChange(value, false);
this.props.onChange(value, false, undefined);
this.setState({ value, cardType: undefined });
return;
}

const { card, isValid, isPotentiallyValid } = number(value);

let cardType = undefined;
if (card && card.type && includes(this.props.allowedBrands, card.type)) {
cardType = typeToIconName(card.type);
let cardTypeIconName = undefined;
let cardTypeIsAllowed = false;

if (card && card.type) {
cardTypeIconName = typeToIconName(card.type);
cardTypeIsAllowed = includes(this.props.allowedBrands, card.type);
}

const typeInfo = cardTypeInfo(value);
Expand All @@ -70,15 +73,15 @@ export default class CreditCardNumber extends Component {
}

// Only accept the change if we recognize the card type, and it is/may be valid
if (!this.props.restrictInput || cardType && (isValid || isPotentiallyValid)) {
this.props.onChange(value, isValid);
this.setState({ value, cardType, isValid });
if (!this.props.restrictInput || cardTypeIsAllowed && (isValid || isPotentiallyValid)) {
this.props.onChange({ cardNumber: value, cardType: card.type }, isValid);
this.setState({ value, cardTypeIconName, isValid });
}
}

render() {
const { placeholder } = this.props;
const { cardType, value } = this.state;
const { cardTypeIconName, value } = this.state;

return (
<InputGroup className="credit-card-number-field">
Expand All @@ -87,9 +90,9 @@ export default class CreditCardNumber extends Component {
placeholder={placeholder} value={value}
onChange={this.onInputChange}
/>
{cardType &&
{cardTypeIconName &&
<InputGroupAddon>
<Icon name={cardType} size="lg" />
<Icon name={cardTypeIconName} size="lg" />
</InputGroupAddon>
}
</InputGroup>
Expand All @@ -103,7 +106,7 @@ CreditCardNumber.defaultProps = {
restrictInput: false,
value: '',

onChange: (cardNumber, isValid) => true, // eslint-disable-line no-unused-vars
onChange: (cardNumber, isValid, cardType) => true, // eslint-disable-line no-unused-vars
};
CreditCardNumber.propTypes = {
allowedBrands: PropTypes.arrayOf(PropTypes.string),
Expand Down
25 changes: 18 additions & 7 deletions test/components/CreditCardNumber.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@
import React from 'react';
import assert from 'assert';
import { mount, shallow } from 'enzyme';
import sinon from 'sinon';

import CreditCardNumber from '../../src/components/CreditCardNumber';
import Icon from '../../src/components/Icon';

const EXAMPLES = {
'american-express': '378282246310005',
'diners-club': '30569309025904',
amex: '378282246310005',
'master-card': '5555555555554444',
discover: '6011111111111117',
jcb: '3530111333300000',
mastercard: '5555555555554444',
visa: '4111111111111111',
};
const ICON_MAP = {
'american-express': 'amex',
'master-card': 'mastercard',
};

describe('<CreditCardNumber />', () => {
it('should render no icon, by default', () => {
Expand All @@ -30,16 +35,22 @@ describe('<CreditCardNumber />', () => {
assert.equal(component.find(Icon).prop('name'), 'cc-visa');
});

it('should render correct icons for valid card numbers', () => {
it('should report/render icon for correct cardType for valid numbers', () => {
Object.keys(EXAMPLES).forEach(key => {
const expectedIcon = `cc-${key}`;
const cardNumber = EXAMPLES[key];
const onChange = sinon.spy();

const component = mount(<CreditCardNumber />);
const component = mount(<CreditCardNumber onChange={onChange} />);
const input = component.find('input');
input.simulate('change', { target: { value: cardNumber } });

assert.equal(component.find(Icon).prop('name'), expectedIcon);
assert(onChange.called);
const [values, returnedIsValid] = [...onChange.lastCall.args];
assert.equal(values.cardNumber.replace(/ /g, ''), cardNumber);
assert.equal(values.cardType, key);
assert.equal(returnedIsValid, true);

assert.equal(component.find(Icon).prop('name'), `cc-${ICON_MAP[key] || key}`);
});
});

Expand All @@ -65,7 +76,7 @@ describe('<CreditCardNumber />', () => {
it('restrictInput prop should reject numbers for invalid card types', () => {
const component = mount(<CreditCardNumber restrictInput allowedBrands={['visa']} />);
const input = component.find('input');
input.simulate('change', { target: { value: EXAMPLES.mastercard } });
input.simulate('change', { target: { value: EXAMPLES['master-card'] } });

assert.equal(input.get(0).value, '');
assert.equal(component.find(Icon).length, 0);
Expand Down

0 comments on commit 5c40623

Please sign in to comment.