#!/usr/bin/env node // Native const { resolve } = require('path') // Packages const chalk = require('chalk') const minimist = require('minimist') const ms = require('ms') // Ours const login = require('../lib/login') const cfg = require('../lib/cfg') const { 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 argv = minimist(process.argv.slice(2), { string: ['config', 'token'], boolean: ['help', 'debug'], alias: { help: 'h', config: 'c', debug: 'd', token: 't' } }) const subcommand = argv._[0] const help = () => { console.log(` ${chalk.bold(`${logo} now billing`)} ${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 `)} ${chalk.gray('–')} If the id is omitted, you can choose interactively ${chalk.gray('–')} Selects your default credit card: ${chalk.cyan(`$ now billing set-default `)} ${chalk.gray('–')} If the id is omitted, you can choose interactively `) } // Options const debug = argv.debug const apiUrl = argv.url || 'https://api.zeit.co' if (argv.config) { cfg.setConfigFile(argv.config) } 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) } if (argv.help || !subcommand) { help() exit(0) } else { Promise.resolve().then(async () => { 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) } }) } // 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 `) 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 `) 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': { require(resolve(__dirname, 'now-billing-add.js'))({ creditCards, currentTeam, user }) break } default: error('Please specify a valid subcommand: ls | add | rm | set-default') help() exit(1) } creditCards.close() }