Browse Source

Implemented the last sub command

master
Leo Lamprecht 7 years ago
parent
commit
ded122806f
  1. 22
      package-lock.json
  2. 2
      package.json
  3. 389
      src/providers/sh/commands/bin/billing.js
  4. 255
      src/providers/sh/commands/bin/billing/add.js
  5. 3
      src/providers/sh/commands/bin/domains/buy.js

22
package-lock.json

@ -4,6 +4,12 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@google/maps": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@google/maps/-/maps-0.4.3.tgz",
"integrity": "sha1-tGiWERTW4HScFYAuDaFJ3Ooig6U=",
"dev": true
},
"acorn": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz",
@ -1842,6 +1848,16 @@
"sha.js": "2.4.8"
}
},
"credit-card": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/credit-card/-/credit-card-3.0.1.tgz",
"integrity": "sha1-uY842UrfMnXxQJHYvSzRQcx4nFY=",
"dev": true,
"requires": {
"lodash.merge": "4.6.0",
"reach": "1.0.0"
}
},
"cross-spawn": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",
@ -6434,6 +6450,12 @@
}
}
},
"reach": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/reach/-/reach-1.0.0.tgz",
"integrity": "sha1-TRLRYJYLysZ9xiTw+Ruv6zb3lPY=",
"dev": true
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",

2
package.json

@ -78,6 +78,7 @@
]
},
"devDependencies": {
"@google/maps": "0.4.3",
"ansi-escapes": "2.0.0",
"archiver": "2.0.0",
"arr-flatten": "1.1.0",
@ -97,6 +98,7 @@
"clipboardy": "1.1.4",
"convert-stream": "1.0.2",
"copy-webpack-plugin": "4.0.1",
"credit-card": "3.0.1",
"dateformat": "2.0.0",
"death": "1.1.0",
"debug": "3.0.0",

389
src/providers/sh/commands/bin/billing.js

@ -0,0 +1,389 @@
#!/usr/bin/env node
// Packages
const chalk = require('chalk')
const minimist = require('minimist')
const ms = require('ms')
// Utilities
const login = require('../lib/login')
const cfg = require('../lib/cfg')
const { handleError, error } = require('../lib/error')
const NowCreditCards = require('../lib/credit-cards')
const indent = require('../lib/indent')
const listInput = require('../lib/utils/input/list')
const success = require('../lib/utils/output/success')
const promptBool = require('../lib/utils/input/prompt-bool')
const info = require('../lib/utils/output/info')
const logo = require('../lib/utils/output/logo')
const addBilling = require('./billing/add')
const help = () => {
console.log(`
${chalk.bold(`${logo} now billing`)} <ls | add | rm | set-default>
${chalk.dim('Options:')}
-h, --help Output usage information
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline(
'FILE'
)} Config file
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
${chalk.dim('Examples:')}
${chalk.gray('–')} Lists all your credit cards:
${chalk.cyan('$ now billing ls')}
${chalk.gray('–')} Adds a credit card (interactively):
${chalk.cyan(`$ now billing add`)}
${chalk.gray('–')} Removes a credit card:
${chalk.cyan(`$ now billing rm <id>`)}
${chalk.gray('–')} If the id is omitted, you can choose interactively
${chalk.gray('–')} Selects your default credit card:
${chalk.cyan(`$ now billing set-default <id>`)}
${chalk.gray('–')} If the id is omitted, you can choose interactively
`)
}
const exit = code => {
// We give stdout some time to flush out
// because there's a node bug where
// stdout writes are asynchronous
// https://github.com/nodejs/node/issues/6456
setTimeout(() => process.exit(code || 0), 100)
}
let argv
let debug
let apiUrl
let subcommand
const main = async ctx => {
argv = minimist(ctx.argv.slice(2), {
string: ['config', 'token'],
boolean: ['help', 'debug'],
alias: {
help: 'h',
config: 'c',
debug: 'd',
token: 't'
}
})
argv._ = argv._.slice(1)
debug = argv.debug
apiUrl = argv.url || 'https://api.zeit.co'
subcommand = argv._[0]
if (argv.config) {
cfg.setConfigFile(argv.config)
}
if (argv.help || !subcommand) {
help()
exit(0)
}
const config = await cfg.read({ token: argv.token })
let token
try {
token = config.token || (await login(apiUrl))
} catch (err) {
error(`Authentication error – ${err.message}`)
exit(1)
}
try {
await run({ token, config })
} catch (err) {
if (err.userError) {
error(err.message)
} else {
error(`Unknown error: ${err.stack}`)
}
exit(1)
}
}
module.exports = async ctx => {
try {
await main(ctx)
} catch (err) {
handleError(err)
process.exit(1)
}
}
// Builds a `choices` object that can be passesd to inquirer.prompt()
function buildInquirerChoices(cards) {
return cards.cards.map(card => {
const _default =
card.id === cards.defaultCardId ? ' ' + chalk.bold('(default)') : ''
const id = `${chalk.cyan(`ID: ${card.id}`)}${_default}`
const number = `${chalk.gray('#### ').repeat(3)}${card.last4}`
const str = [
id,
indent(card.name, 2),
indent(`${card.brand} ${number}`, 2)
].join('\n')
return {
name: str, // Will be displayed by Inquirer
value: card.id, // Will be used to identify the answer
short: card.id // Will be displayed after the users answers
}
})
}
async function run({ token, config: { currentTeam, user } }) {
const start = new Date()
const creditCards = new NowCreditCards({ apiUrl, token, debug, currentTeam })
const args = argv._.slice(1)
switch (subcommand) {
case 'ls':
case 'list': {
let cards
try {
cards = await creditCards.ls()
} catch (err) {
error(err.message)
return
}
const text = cards.cards
.map(card => {
const _default =
card.id === cards.defaultCardId ? ' ' + chalk.bold('(default)') : ''
const id = `${chalk.gray('-')} ${chalk.cyan(
`ID: ${card.id}`
)}${_default}`
const number = `${chalk.gray('#### ').repeat(3)}${card.last4}`
let address = card.address_line1
if (card.address_line2) {
address += `, ${card.address_line2}.`
} else {
address += '.'
}
address += `\n${card.address_city}, `
if (card.address_state) {
address += `${card.address_state}, `
}
// Stripe is returning a two digit code for the country,
// but we want the full country name
address += `${card.address_zip}. ${card.address_country}`
return [
id,
indent(card.name, 2),
indent(`${card.brand} ${number}`, 2),
indent(address, 2)
].join('\n')
})
.join('\n\n')
const elapsed = ms(new Date() - start)
console.log(
`> ${cards.cards.length} card${cards.cards.length === 1
? ''
: 's'} found under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)} ${chalk.gray(`[${elapsed}]`)}`
)
if (text) {
console.log(`\n${text}\n`)
}
break
}
case 'set-default': {
if (args.length > 1) {
error('Invalid number of arguments')
return exit(1)
}
const start = new Date()
let cards
try {
cards = await creditCards.ls()
} catch (err) {
error(err.message)
return
}
if (cards.cards.length === 0) {
error('You have no credit cards to choose from')
return exit(0)
}
let cardId = args[0]
if (cardId === undefined) {
const elapsed = ms(new Date() - start)
const message = `Selecting a new default payment card for ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)} ${chalk.gray(`[${elapsed}]`)}`
const choices = buildInquirerChoices(cards)
cardId = await listInput({
message,
choices,
separator: true,
abort: 'end'
})
}
// Check if the provided cardId (in case the user
// typed `now billing set-default <some-id>`) is valid
if (cardId) {
const label = `Are you sure that you to set this card as the default?`
const confirmation = await promptBool(label, {
trailing: '\n'
})
if (!confirmation) {
info('Aborted')
break
}
const start = new Date()
await creditCards.setDefault(cardId)
const card = cards.cards.find(card => card.id === cardId)
const elapsed = ms(new Date() - start)
success(
`${card.brand} ending in ${card.last4} is now the default ${chalk.gray(
`[${elapsed}]`
)}`
)
} else {
console.log('No changes made')
}
break
}
case 'rm':
case 'remove': {
if (args.length > 1) {
error('Invalid number of arguments')
return exit(1)
}
const start = new Date()
let cards
try {
cards = await creditCards.ls()
} catch (err) {
error(err.message)
return
}
if (cards.cards.length === 0) {
error(
`You have no credit cards to choose from to delete under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}`
)
return exit(0)
}
let cardId = args[0]
if (cardId === undefined) {
const elapsed = ms(new Date() - start)
const message = `Selecting a card to ${chalk.underline(
'remove'
)} under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)} ${chalk.gray(`[${elapsed}]`)}`
const choices = buildInquirerChoices(cards)
cardId = await listInput({
message,
choices,
separator: true,
abort: 'start'
})
}
// Shoud check if the provided cardId (in case the user
// typed `now billing rm <some-id>`) is valid
if (cardId) {
const label = `Are you sure that you want to remove this card?`
const confirmation = await promptBool(label)
if (!confirmation) {
console.log('Aborted')
break
}
const start = new Date()
await creditCards.rm(cardId)
const deletedCard = cards.cards.find(card => card.id === cardId)
const remainingCards = cards.cards.filter(card => card.id !== cardId)
let text = `${deletedCard.brand} ending in ${deletedCard.last4} was deleted`
// ${chalk.gray(`[${elapsed}]`)}
if (cardId === cards.defaultCardId) {
if (remainingCards.length === 0) {
// The user deleted the last card in their account
text += `\n${chalk.yellow('Warning!')} You have no default card`
} else {
// We can't guess the current default card – let's ask the API
const cards = await creditCards.ls()
const newDefaultCard = cards.cards.find(
card => card.id === cards.defaultCardId
)
text += `\n${newDefaultCard.brand} ending in ${newDefaultCard.last4} in now default for ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}`
}
}
const elapsed = ms(new Date() - start)
text += ` ${chalk.gray(`[${elapsed}]`)}`
success(text)
} else {
console.log('No changes made')
}
break
}
case 'add': {
addBilling({
creditCards,
currentTeam,
user
})
break
}
default:
error('Please specify a valid subcommand: ls | add | rm | set-default')
help()
exit(1)
}
creditCards.close()
}

255
src/providers/sh/commands/bin/billing/add.js

@ -0,0 +1,255 @@
#!/usr/bin/env node
// Packages
const ansiEscapes = require('ansi-escapes')
const chalk = require('chalk')
const ccValidator = require('credit-card')
// Utilities
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')
const { tick } = require('../../lib/utils/output/chars')
const rightPad = require('../../lib/utils/output/right-pad')
function expDateMiddleware(data) {
return data
}
module.exports = async function({
creditCards,
currentTeam,
user,
clear = false
}) {
const state = {
error: undefined,
cardGroupLabel: `> ${chalk.bold(
`Enter your card details for ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}`
)}`,
name: {
label: rightPad('Full Name', 12),
placeholder: 'John Appleseed',
validateValue: data => data.trim().length > 0
},
cardNumber: {
label: rightPad('Number', 12),
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', 12),
mask: 'ccv',
placeholder: '###',
validateValue: data => {
const brand = state.cardNumber.brand.toLowerCase()
return ccValidator.doesCvvMatchType(data, brand)
}
},
expDate: {
label: rightPad('Exp. Date', 12),
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', 12),
async autoComplete(value) {
for (const country in countries) {
if (!Object.hasOwnProperty.call(countries, country)) {
continue
}
if (country.startsWith(value)) {
return country.substr(value.length)
}
const lowercaseCountry = country.toLowerCase()
const lowercaseValue = value.toLowerCase()
if (lowercaseCountry.startsWith(lowercaseValue)) {
return lowercaseCountry.substr(value.length)
}
}
return false
},
validateValue: value => {
for (const country in countries) {
if (!Object.hasOwnProperty.call(countries, country)) {
continue
}
if (country.toLowerCase() === value.toLowerCase()) {
return true
}
}
return false
}
},
zipCode: {
label: rightPad('ZIP', 12),
validadeKeypress: data => data.trim().length > 0,
validateValue: data => data.trim().length > 0
},
state: {
label: rightPad('State', 12),
validateValue: data => data.trim().length > 0
},
city: {
label: rightPad('City', 12),
validateValue: data => data.trim().length > 0
},
address1: {
label: rightPad('Address', 12),
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 {
/* eslint-disable no-await-in-loop */
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(tick)} ${piece.label}${masked} ${brand}\n`
)
} else if (key === 'ccv') {
process.stdout.write(
`${chalk.cyan(tick)} ${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(tick)} ${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(tick)} ${piece.label}${result}\n`
)
} else {
process.stdout.write(
`${chalk.cyan(tick)} ${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()
if (clear) {
const linesToClear = state.error ? 15 : 14
process.stdout.write(ansiEscapes.eraseLines(linesToClear))
}
success(
`${state.cardNumber
.brand} ending in ${res.last4} was added to ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}`
)
} catch (err) {
stopSpinner()
const linesToClear = state.error ? 15 : 14
process.stdout.write(ansiEscapes.eraseLines(linesToClear))
state.error = `${chalk.red(
'> Error!'
)} ${err.message} Please make sure the info is correct`
await render()
}
}
try {
await render()
} catch (err) {
console.erorr(err)
}
}

3
src/providers/sh/commands/bin/domains/buy.js

@ -13,6 +13,7 @@ const promptBool = require('../../lib/utils/input/prompt-bool')
const eraseLines = require('../../lib/utils/output/erase-lines')
const treatBuyError = require('../../lib/utils/domains/treat-buy-error')
const NowCreditCards = require('../../lib/credit-cards')
const addBilling = require('../billing/add')
module.exports = async function({ domains, args, currentTeam, user, coupon }) {
const name = args[0]
@ -59,7 +60,7 @@ module.exports = async function({ domains, args, currentTeam, user, coupon }) {
)
info(`Your card will ${bold('not')} be charged`)
await require('../now-billing-add')({
await addBilling({
creditCards,
currentTeam,
user,

Loading…
Cancel
Save