UNPKG

react-saasify

Version:

React components for Saasify web clients.

418 lines (366 loc) 10.7 kB
import React, { Component, Fragment } from 'react' import PropTypes from 'prop-types' import { Button, Divider, Icon, Modal, Table, Tooltip, notification } from 'lib/antd' import { reaction } from 'mobx' import { observer, inject } from 'mobx-react' import { Section } from '../Section' import { CheckoutForm } from '../CheckoutForm' import API from 'lib/api' import theme from 'lib/theme' import styles from './styles.module.css' // TODO: fix minor css jank with fixed header when opening modal @inject('auth') @observer export class BillingSourcesSection extends Component { static propTypes = { auth: PropTypes.object.isRequired } state = { data: [], hasMoreData: true, isLoading: false, isVisibleAddNewSourceModal: false, isVisibleRemoveSourceModal: false, isVisibleSetDefaultSourceModal: false, isLoadingAddNewSourceModal: false, isLoadingRemoveSourceModal: false, isLoadingSetDefaultSourceModal: false, selectedSource: null } _columns = [ { title: 'Brand', dataIndex: 'brand' }, { title: 'Number', dataIndex: 'last4', render: (last4) => `XXXX - ${last4}` }, { title: 'Name', dataIndex: 'name' }, { title: 'Expiration', key: 'expiration', render: (card) => `${card.exp_month} / ${card.exp_year}` }, { title: 'Zipcode', dataIndex: 'address_zip' }, { title: 'Default', dataIndex: 'default', render: (isDefault) => isDefault ? ( <Icon type='check-circle' theme='twoTone' twoToneColor='#52c41a' style={{ fontSize: '1.8em' }} /> ) : ( '' ) }, { title: 'Actions', key: 'actions', render: (card) => ( <Fragment> <Tooltip title={card.default ? 'Unable to remove default card' : undefined} > <Button type='link' disabled={card.default} onClick={() => this._onClickRemoveSourceButton(card)} > Remove </Button> </Tooltip> <Divider type='vertical' /> <Tooltip title={ card.default ? 'This card is already the default' : undefined } > <Button type='link' disabled={card.default} onClick={() => this._onClickSetDefaultSourceButton(card)} > Set Default </Button> </Tooltip> </Fragment> ) } ] componentDidMount() { this._fetch() } componentWillUnmount() { this._disposer() } _disposer = reaction( () => this.props.auth.user, () => this._fetch() ) render() { const { auth, ...rest } = this.props const { data, isLoading, isVisibleAddNewSourceModal, isVisibleRemoveSourceModal, isVisibleSetDefaultSourceModal, isLoadingAddNewSourceModal, isLoadingRemoveSourceModal, isLoadingSetDefaultSourceModal, selectedSource } = this.state return ( <Section id='billing-sources' title='Payment Methods' {...rest}> <div className={theme(styles, 'body')}> <Table columns={this._columns} rowKey={(record) => record.id} dataSource={data} pagination={false} loading={isLoading} onChange={this._handleTableChange} /> <Button type='primary' className={theme(styles, 'addSourceButton')} onClick={this._onClickAddNewSourceButton} > Add New Payment Method </Button> </div> <Modal title='Add New Payment Method' visible={isVisibleAddNewSourceModal} okButtonProps={{ disabled: true }} confirmLoading={isLoadingAddNewSourceModal} onCancel={this._onCancelNewSourceModal} > {isVisibleAddNewSourceModal && ( <CheckoutForm action='Add Card' loading={isLoadingAddNewSourceModal} onSubmit={this._onConfirmNewSourceModal} /> )} </Modal> <Modal title='Remove Payment Method' visible={isVisibleRemoveSourceModal} onOk={this._onConfirmRemoveSourceModal} confirmLoading={isLoadingRemoveSourceModal} onCancel={this._onCancelRemoveSourceModal} > {selectedSource && ( <div> Are you sure you want to remove the {selectedSource.brand} card ending in <b>{selectedSource.last4}</b> from your payment methods? </div> )} </Modal> <Modal title='Set Default Payment Method' visible={isVisibleSetDefaultSourceModal} onOk={this._onConfirmSetDefaultSourceModal} confirmLoading={isLoadingSetDefaultSourceModal} onCancel={this._onCancelSetDefaultSourceModal} > {selectedSource && ( <div> Are you sure you want to set the {selectedSource.brand} card ending in <b>{selectedSource.last4}</b> as your default payment method? </div> )} </Modal> </Section> ) } _handleTableChange = (pagination, filters, sorter) => { this._fetch({ results: pagination.pageSize, page: pagination.current, sortField: sorter.field, sortOrder: sorter.order, ...filters }) } _fetch = (params = {}) => { const { auth } = this.props if (!auth.user) { return } let { data, hasMoreData } = this.state if (!data.length && hasMoreData) { this.setState({ isLoading: true }) API.listBillingSources().then( (sources) => { if (!sources.length) { hasMoreData = false } else { sources.sort((a, b) => a.id.localeCompare(b.id)) data = data.concat(sources) } this.setState({ isLoading: false, data, hasMoreData }) }, (err) => { console.warn(err) this.setState({ isLoading: false }) } ) } } _onClickAddNewSourceButton = () => { this.setState({ isVisibleAddNewSourceModal: true }) } _onConfirmNewSourceModal = async ({ name, stripe }) => { this.setState({ isLoadingAddNewSourceModal: true }) try { const { token, error } = await stripe.createToken({ name }) console.log({ token, error }) if (error) { notification.error({ message: 'Error processing payment method', description: error.message, duration: 0 }) this.setState({ isLoadingAddNewSourceModal: false }) return } const source = await API.addBillingSource({ source: token.id }) console.log('checkout source', { source }) this.setState( { data: [], hasMoreData: true, isLoadingAddNewSourceModal: false, isVisibleAddNewSourceModal: false }, () => { notification.success({ message: 'Payment Method Added', description: `Your ${source.brand} card ending in ${source.last4} was added successfully.` }) this._fetch() } ) } catch (err) { notification.error({ message: 'Error adding payment source', description: err.error && err.error.message, duration: 0 }) this.setState({ isLoadingAddNewSourceModal: false }) } } _onCancelNewSourceModal = () => { this.setState({ isVisibleAddNewSourceModal: false }) } _onClickRemoveSourceButton = (selectedSource) => { this.setState({ isVisibleRemoveSourceModal: true, selectedSource }) } _onConfirmRemoveSourceModal = () => { const { selectedSource } = this.state this.setState({ isLoadingRemoveSourceModal: true }) API.removeBillingSource(selectedSource.id).then( () => { this.setState( { data: [], hasMoreData: true, isLoadingRemoveSourceModal: false, isVisibleRemoveSourceModal: false, selectedSource: null }, () => { notification.success({ message: 'Card Removed', description: `Your ${selectedSource.brand} card ending in ${selectedSource.last4} has been removed.` }) this._fetch() } ) }, (err) => { console.warn(err) this.setState({ isLoadingRemoveSourceModal: false }) notification.error({ message: 'Error', description: `There was an error trying to remove your ${selectedSource.brand} card ending in ${selectedSource.last4}.`, duration: 10 }) } ) } _onCancelRemoveSourceModal = () => { this.setState({ isVisibleRemoveSourceModal: false, selectedSource: null }) } _onClickSetDefaultSourceButton = (selectedSource) => { this.setState({ isVisibleSetDefaultSourceModal: true, selectedSource }) } _onConfirmSetDefaultSourceModal = () => { const { selectedSource } = this.state this.setState({ isLoadingSetDefaultSourceModal: true }) API.setDefaultBillingSource(selectedSource.id).then( () => { this.setState( { data: [], hasMoreData: true, isLoadingSetDefaultSourceModal: false, isVisibleSetDefaultSourceModal: false, selectedSource: null }, () => { notification.success({ message: 'Card Set as Default', description: `Your ${selectedSource.brand} card ending in ${selectedSource.last4} has been set as your default payment method.` }) this._fetch() } ) }, (err) => { console.warn(err) this.setState({ isLoadingRemoveSourceModal: false }) notification.error({ message: 'Error', description: `There was an error trying to set the ${selectedSource.brand} card ending in ${selectedSource.last4} as your default payment method.`, duration: 10 }) } ) } _onCancelSetDefaultSourceModal = () => { this.setState({ isVisibleSetDefaultSourceModal: false, selectedSource: null }) } }