react-saasify
Version:
React components for Saasify web clients.
418 lines (366 loc) • 10.7 kB
JavaScript
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
('auth')
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
})
}
}