Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace the payment dependency with credit-card-type and luhn #77

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ Or you can import the CSS:

### Features

- We support all credit card issuers available in [Payment](https://github.com/jessepollak/payment) plus Hipercard (a brazilian credit card).
- We support all credit card issuers available in [credit-card-type](https://github.com/braintree/credit-card-type) plus
Dankort, Laser, and Visa Electron.

## Props

Expand Down Expand Up @@ -136,12 +137,32 @@ Or you can import the CSS:

Here's how you can get started developing locally:

1. Clone this repo and link it to your global `node_modules`:

$ git clone https://github.com/amarofashion/react-credit-cards.git
$ cd react-credit-cards
$ npm install
$ npm link

2. Download the demo source from [codesandbox](https://codesandbox.io/s/ovvwzkzry9).
3. Unzip it to the desired directory.
4. Install the dependencies

cassiocardoso marked this conversation as resolved.
Show resolved Hide resolved
$ cd react-credit-cards-demo
$ npm install
$ npm link react-credit-cards

5. On the `react-credit-cards` directory, start the watcher:

$ npm run watch

6. On the `react-credit-cards-demo` directory, start the demo app:

$ npm start

7. 🎉 Done! The demo app will be running on: `http://localhost:3000/`. Your local changes should be automatically reflected there.

Now, if you go to `http://localhost:3000` in your browser, you should see the demo page.
Check [npm-link](https://docs.npmjs.com/cli/link.html) for detailed instructions.

## Contributing

Expand Down
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"contributors": [
{
"name": "Cassio Cardoso",
"email": "cassio.cardoso@amaro.com"
"email": "caugusto.cardoso@gmail.com"
gilbarbara marked this conversation as resolved.
Show resolved Hide resolved
},
{
"name": "Gil Barbara",
Expand Down Expand Up @@ -41,6 +41,8 @@
"prop-types": "^15.6.2"
},
"dependencies": {
"credit-card-type": "^8.3.0",
"luhn": "^2.4.1",
"payment": "^2.3.0"
},
"devDependencies": {
Expand Down
133 changes: 68 additions & 65 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import Payment from 'payment';

class ReactCreditCards extends React.Component {
constructor(props) {
super(props);

this.setCards();
}

static propTypes = {
gilbarbara marked this conversation as resolved.
Show resolved Hide resolved
acceptedCards: PropTypes.array,
callback: PropTypes.func,
cvc: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
expiry: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
focused: PropTypes.string,
issuer: PropTypes.string,
locale: PropTypes.shape({
valid: PropTypes.string,
}),
name: PropTypes.string.isRequired,
number: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
placeholders: PropTypes.shape({
name: PropTypes.string,
}),
preview: PropTypes.bool,
};

static defaultProps = {
acceptedCards: [],
locale: {
valid: 'valid thru',
},
placeholders: {
name: 'YOUR NAME HERE',
},
preview: false,
};
import { cardTypesMap, getCardType, setInitialValidCardTypes, validateLuhn } from './utils/cardHelpers';

class ReactCreditCards extends React.Component {
componentDidUpdate(prevProps) {
const { acceptedCards, callback, number } = this.props;

if (prevProps.number !== number) {
/* istanbul ignore else */
if (typeof callback === 'function') {
callback(this.options, Payment.fns.validateCardNumber(number));
callback(this.options, validateLuhn(number));
}
}

if (prevProps.acceptedCards.toString() !== acceptedCards.toString()) {
this.setCards();
this.updateValidCardTypes(acceptedCards);
}
}

Expand Down Expand Up @@ -90,7 +47,7 @@ class ReactCreditCards extends React.Component {
nextNumber += '•';
}

if (['amex', 'dinersclub'].includes(this.issuer)) {
cassiocardoso marked this conversation as resolved.
Show resolved Hide resolved
if (cardTypesMap.amex.includes(this.issuer) || cardTypesMap.dinersclub.includes(this.issuer)) {
const format = [0, 4, 10];
const limit = [4, 6, 5];
nextNumber = `${nextNumber.substr(format[0], limit[0])} ${nextNumber.substr(format[1], limit[1])} ${nextNumber.substr(format[2], limit[2])}`;
Expand Down Expand Up @@ -141,43 +98,51 @@ class ReactCreditCards extends React.Component {

get options() {
const { number } = this.props;
const issuer = Payment.fns.cardType(number) || 'unknown';
let updatedIssuer = 'unknown';

if (number) {
const validatedIssuer = getCardType(number);

if (this.validCardTypes.includes(validatedIssuer)) {
updatedIssuer = validatedIssuer;
}
}

let maxLength = 16;

if (issuer === 'amex') {
if (cardTypesMap.amex.includes(updatedIssuer)) {
maxLength = 15;
}
else if (issuer === 'dinersclub') {
else if (cardTypesMap.dinersclub.includes(updatedIssuer)) {
maxLength = 14;
}
else if (['hipercard', 'mastercard', 'visa'].includes(issuer)) {
else if (['hipercard', 'mastercard', 'visa'].includes(updatedIssuer)) {
maxLength = 19;
}

return {
issuer,
issuer: updatedIssuer,
maxLength,
};
}

setCards() {
get validCardTypes() {
const { acceptedCards } = this.props;
let newCardArray = [];
const initialValidCardTypes = setInitialValidCardTypes();

if (acceptedCards.length) {
Payment.getCardArray()
.forEach(d => {
if (acceptedCards.includes(d.type)) {
newCardArray.push(d);
}
});
return initialValidCardTypes.filter(card => acceptedCards.includes(card));
}
else {
newCardArray = newCardArray.concat(Payment.getCardArray());

return initialValidCardTypes;
}

updateValidCardTypes(acceptedCards) {
if (acceptedCards.length) {
return this.validCardTypes.filter(card => acceptedCards.includes(card));
}

Payment.setCardArray(newCardArray);
return this.validCardTypes;
}

render() {
Expand All @@ -190,7 +155,7 @@ class ReactCreditCards extends React.Component {
className={[
'rccs__card',
`rccs__card--${this.issuer}`,
focused === 'cvc' && this.issuer !== 'amex' ? 'rccs__card--flipped' : '',
focused === 'cvc' && this.issuer !== 'american-express' ? 'rccs__card--flipped' : '',
].join(' ').trim()}
>
<div className="rccs__card--front">
Expand Down Expand Up @@ -255,4 +220,42 @@ class ReactCreditCards extends React.Component {
}
}

ReactCreditCards.propTypes = {
acceptedCards: PropTypes.array,
callback: PropTypes.func,
cvc: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
expiry: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
focused: PropTypes.string,
issuer: PropTypes.string,
locale: PropTypes.shape({
valid: PropTypes.string,
}),
name: PropTypes.string.isRequired,
number: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
placeholders: PropTypes.shape({
name: PropTypes.string,
}),
preview: PropTypes.bool,
};

ReactCreditCards.defaultProps = {
acceptedCards: [],
locale: {
valid: 'valid thru',
},
placeholders: {
name: 'YOUR NAME HERE',
},
preview: false,
};

export default ReactCreditCards;
28 changes: 14 additions & 14 deletions src/styles.scss

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions src/utils/cardHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import creditCardType, { types as cardTypes } from 'credit-card-type';
import luhn from 'luhn';

import { dankort, laser, visaElectron } from './cardTypes';

/**
* Check if a credit card number is valid using the Luhn algorithm
* @returns {boolean}
*/
export const validateLuhn = luhn.validate;

/**
* Given a credit card number in the format (XXXX XXXX XXXX...) return it as a string without any spaces
* @param {*} number
* @returns {string} number
*/
export const sanitizeNumber = (number) => number.toString().trim().replace(' ', '');

/**
* Return the issuer of a given credit card number or `unknown` if the issuer can't be identified
* @param {string|number} cardNumber
* @returns {string} cardType
*/
export const getCardType = (cardNumber) => {
const potentialCardTypes = creditCardType(sanitizeNumber(cardNumber));

if (potentialCardTypes.length === 1) {
const firstResult = potentialCardTypes.shift();

return firstResult.type;
}

return 'unknown';
};

/**
* Configure the credit card types supported and return an array of valid types
* @returns {string[]} validCardTypes
*/
export const setInitialValidCardTypes = () => {
creditCardType.updateCard(cardTypes.MAESTRO, {
patterns: [
493698,
[5000, 5018],
[502000, 506698],
[506779, 508999],
[56, 59],
63,
67,
6,
],
});

creditCardType.updateCard(cardTypes.HIPERCARD, {
patterns: [
384100,
384140,
384160,
606282,
637095,
637568,
],
});

creditCardType.addCard(dankort);
creditCardType.addCard(laser);
creditCardType.addCard(visaElectron);

return Object.values(cardTypes).concat(['dankort', 'laser', 'visa-electron']);
};

/**
* Provides a map of patterns to match for some card types
*/
export const cardTypesMap = {
amex: ['amex', 'americanexpress', 'american-express'],
dinersclub: ['diners', 'dinersclub', 'diners-club'],
visaelectron: ['visaelectron', 'visa-electron'],
};
Loading