|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
// Packages
|
|
|
|
const ansiEscapes = require("ansi-escapes");
|
|
|
|
const chalk = require("chalk");
|
|
|
|
const ccValidator = require("credit-card");
|
|
|
|
|
|
|
|
// Ours
|
|
|
|
const textInput = require("../lib/utils/input/text");
|
|
|
|
const countries = require("../lib/utils/billing/country-list");
|
|
|
|
const cardBrands = require("../lib/utils/billing/card-brands");
|
|
|
|
const geocode = require("../lib/utils/billing/geocode");
|
|
|
|
const success = require("../lib/utils/output/success");
|
|
|
|
const wait = require("../lib/utils/output/wait");
|
|
|
|
|
|
|
|
function rightPad(string, n = 12) {
|
|
|
|
n -= string.length;
|
|
|
|
return string + " ".repeat(n > -1 ? n : 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
function expDateMiddleware(data) {
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = function(creditCards) {
|
|
|
|
const state = {
|
|
|
|
error: undefined,
|
|
|
|
cardGroupLabel: `> ${chalk.bold("Enter your card details")}`,
|
|
|
|
|
|
|
|
name: {
|
|
|
|
label: rightPad("Full Name"),
|
|
|
|
placeholder: "John Appleseed",
|
|
|
|
validateValue: data => data.trim().length > 0
|
|
|
|
},
|
|
|
|
|
|
|
|
cardNumber: {
|
|
|
|
label: rightPad("Number"),
|
|
|
|
mask: "cc",
|
|
|
|
placeholder: "#### #### #### ####",
|
|
|
|
validateKeypress: (data, value) => /\d/.test(data) && value.length < 19,
|
|
|
|
validateValue: data => {
|
|
|
|
data = data.replace(/ /g, "");
|
|
|
|
const type = ccValidator.determineCardType(data);
|
|
|
|
if (!type) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return ccValidator.isValidCardNumber(data, type);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
ccv: {
|
|
|
|
label: rightPad("CCV"),
|
|
|
|
mask: "ccv",
|
|
|
|
placeholder: "###",
|
|
|
|
validateValue: data => {
|
|
|
|
const brand = state.cardNumber.brand.toLowerCase();
|
|
|
|
return ccValidator.doesCvvMatchType(data, brand);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
expDate: {
|
|
|
|
label: rightPad("Exp. Date"),
|
|
|
|
mask: "expDate",
|
|
|
|
placeholder: "mm / yyyy",
|
|
|
|
middleware: expDateMiddleware,
|
|
|
|
validateValue: data => !ccValidator.isExpired(...data.split(" / "))
|
|
|
|
},
|
|
|
|
|
|
|
|
addressGroupLabel: `\n> ${chalk.bold("Enter your billing address")}`,
|
|
|
|
|
|
|
|
country: {
|
|
|
|
label: rightPad("Country"),
|
|
|
|
async autoComplete(value) {
|
|
|
|
for (const country in countries) {
|
|
|
|
if (!Object.hasOwnProperty.call(countries, country)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (country.startsWith(value)) {
|
|
|
|
return country.substr(value.length);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
validateValue: value => countries[value] !== undefined
|
|
|
|
},
|
|
|
|
|
|
|
|
zipCode: {
|
|
|
|
label: rightPad("ZIP"),
|
|
|
|
validadeKeypress: data => data.trim().length > 0,
|
|
|
|
validateValue: data => data.trim().length > 0
|
|
|
|
},
|
|
|
|
|
|
|
|
state: {
|
|
|
|
label: rightPad("State"),
|
|
|
|
validateValue: data => data.trim().length > 0
|
|
|
|
},
|
|
|
|
|
|
|
|
city: {
|
|
|
|
label: rightPad("City"),
|
|
|
|
validateValue: data => data.trim().length > 0
|
|
|
|
},
|
|
|
|
|
|
|
|
address1: {
|
|
|
|
label: rightPad("Address"),
|
|
|
|
validateValue: data => data.trim().length > 0
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
async function render() {
|
|
|
|
for (const key in state) {
|
|
|
|
if (!Object.hasOwnProperty.call(state, key)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const piece = state[key];
|
|
|
|
if (typeof piece === "string") {
|
|
|
|
console.log(piece);
|
|
|
|
} else if (typeof piece === "object") {
|
|
|
|
let result;
|
|
|
|
try {
|
|
|
|
result = await textInput({
|
|
|
|
label: "- " + piece.label,
|
|
|
|
initialValue: piece.initialValue || piece.value,
|
|
|
|
placeholder: piece.placeholder,
|
|
|
|
mask: piece.mask,
|
|
|
|
validateKeypress: piece.validateKeypress,
|
|
|
|
validateValue: piece.validateValue,
|
|
|
|
autoComplete: piece.autoComplete
|
|
|
|
});
|
|
|
|
piece.value = result;
|
|
|
|
if (key === "cardNumber") {
|
|
|
|
let brand = cardBrands[ccValidator.determineCardType(result)];
|
|
|
|
piece.brand = brand;
|
|
|
|
if (brand === "American Express") {
|
|
|
|
state.ccv.placeholder = "#".repeat(4);
|
|
|
|
} else {
|
|
|
|
state.ccv.placeholder = "#".repeat(3);
|
|
|
|
}
|
|
|
|
brand = chalk.cyan(`[${brand}]`);
|
|
|
|
const masked = chalk.gray("#### ".repeat(3)) + result.split(" ")[3];
|
|
|
|
process.stdout.write(
|
|
|
|
`${chalk.cyan("✓")} ${piece.label}${masked} ${brand}\n`
|
|
|
|
);
|
|
|
|
} else if (key === "ccv") {
|
|
|
|
process.stdout.write(
|
|
|
|
`${chalk.cyan("✓")} ${piece.label}${"*".repeat(result.length)}\n`
|
|
|
|
);
|
|
|
|
} else if (key === "expDate") {
|
|
|
|
let text = result.split(" / ");
|
|
|
|
text = text[0] + chalk.gray(" / ") + text[1];
|
|
|
|
process.stdout.write(`${chalk.cyan("✓")} ${piece.label}${text}\n`);
|
|
|
|
} else if (key === "zipCode") {
|
|
|
|
const stopSpinner = wait(piece.label + result);
|
|
|
|
const addressInfo = await geocode({
|
|
|
|
country: state.country.value,
|
|
|
|
zipCode: result
|
|
|
|
});
|
|
|
|
if (addressInfo.state) {
|
|
|
|
state.state.initialValue = addressInfo.state;
|
|
|
|
}
|
|
|
|
if (addressInfo.city) {
|
|
|
|
state.city.initialValue = addressInfo.city;
|
|
|
|
}
|
|
|
|
stopSpinner();
|
|
|
|
process.stdout.write(
|
|
|
|
`${chalk.cyan("✓")} ${piece.label}${result}\n`
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
process.stdout.write(
|
|
|
|
`${chalk.cyan("✓")} ${piece.label}${result}\n`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
if (err.message === "USER_ABORT") {
|
|
|
|
process.exit(1);
|
|
|
|
} else {
|
|
|
|
console.error(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
console.log(""); // new line
|
|
|
|
const stopSpinner = wait("Saving card");
|
|
|
|
|
|
|
|
try {
|
|
|
|
const res = await creditCards.add({
|
|
|
|
name: state.name.value,
|
|
|
|
cardNumber: state.cardNumber.value,
|
|
|
|
ccv: state.ccv.value,
|
|
|
|
expDate: state.expDate.value,
|
|
|
|
country: state.country.value,
|
|
|
|
zipCode: state.zipCode.value,
|
|
|
|
state: state.state.value,
|
|
|
|
city: state.city.value,
|
|
|
|
address1: state.address1.value
|
|
|
|
});
|
|
|
|
stopSpinner();
|
|
|
|
success(
|
|
|
|
`${state.cardNumber.brand} ending in ${res.last4} was added to your account`
|
|
|
|
);
|
|
|
|
} catch (err) {
|
|
|
|
stopSpinner();
|
|
|
|
const linesToClear = state.error ? 13 : 12;
|
|
|
|
process.stdout.write(ansiEscapes.eraseLines(linesToClear));
|
|
|
|
state.error = `${chalk.red("> Error!")} ${err.message} Please make sure the info is correct`;
|
|
|
|
await render();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
render().catch(console.error);
|
|
|
|
};
|