This project was bootstrapped with Create React App.
Table of Contents
WePay-React-Checkout is a React component that renders a full checkout form and uses WePay to tokenize credit card information. You must have a WePay app to use this library.
The purpose is to provide a pre-made and out of the box checkout form that works for lots of different use cases. Building a checkout form that handles all of the nuances of payments can be challenging and time consuming. WePay offers an iFrame based checkout form that is meant to take care of everything, but it lacks customization. This library provides an out-of-the-box solution that can be configured to meet your specific needs.
The default form here is not perfect, and will not work for everyone, but it wil hopefully serve as a good starting point for lots of platforms looking to accept payments. This library will hopefully grow and expand over time, and offer different flavors of the checkout form. They will all use the same data flow architecture, but offer different features and cosmetic looks to fit more specific needs. For example, the default form doesn't handle the process of collecting shipping information. The WePay API doesn't care about shipping info, but it can be an integral part of your payment flow anyway.
The top level, default form is called PaymentForm
, and is importable by:
import {PaymentForm} from 'wepay-react-checkout'
From there, you can insert the PaymentForm
into a render function. PaymentForm doesn't use any internal state to set it's own values. Everything must come down as a prop
. This allows you to easily prefill info that you may already have.
The snippet below is from an App.js
file created by create-react-app
. We've then imported the PaymenForm
and basic state management. We discuss this in more detail later in the README.
import React, { Component } from 'react';
import './App.css';
import {PaymentForm} from 'wepay-react-checkout';
class App extends Component {
constructor(props) {
super(props);
this.state = {
payment_form: {
payment_info: {},
name: {},
email: {},
}
};
this.handleChange = this.handleChange.bind(this);
this.handleTokenized = this.handleTokenized.bind(this);
}
handleChange(name, value) {
var new_state = this.state;
new_state[name] = value;
this.setState(new_state);
}
// on success occurs when the card is tokenized
// at this point, you need to send the tokenized card to your backend server
// along with the payment information (such as cost)
handleTokenized(response) {
// we are going to re-package the info
// this is all we need to call WePay's /checkout/create
var payload = {
credit_card_id:response.credit_card_id,
amount: this.state.payment_form.amount.value,
currency: this.state.payment_form.amount.currency,
};
alert(`Submit to backend: ${JSON.stringify(payload)}`);
}
render() {
const payment_form = this.state.payment_form ? this.state.payment_form : {};
return (<PaymentForm
client_id="YOUR APP ID"
production={false}
onChange={this.handleChange}
onTokenized={this.handleTokenized}
name={payment_form.name}
email={payment_form.email}
amount={payment_form.amount}
payment_info={payment_form.payment_info}
/>);
}
}
export default App;
The top level, default form is called PaymentForm
, and is importable by:
import {PaymentForm} from 'wepay-react-checkout'
The PaymentForm
itself is a collection of several other subobjects:
Amount
- the amount that the payer will be paying
Email
- the payer's email
Name
- the payer's first and last name
PaymentInfo
- a collection of even more objects that gather all of the credit card related info
CardNumber
- The payer's credit card number
CVC
- The card's CVC
Expiration
- The card's expiration month and year
Address
- a collection of objects that gather all address based information
BillingAddress
- The street address of the payer
City
- The city of the payer
State
- The state (or region depending on country) of the payer
PostalCode
- The zip-code (or postal-code depending on country) of the payer
Not all of this information is required for working with WePay depending on your integration. The address information is the most variable. It is recommended that you collect a full address for a payer not located in the US, but only Country
and PostalCode
are required.
Each of these components extends our Base
component which is responsible for the basic data flow. All of these components take values in as props
and do use an internal state to set their own values.
The Base
component is extremely simply and the most important part is that it contains a handleChange
function. This function is responsible for aggregating state values and passing them up to the parent component. All values are passed up as objects of the format:
{componentName:{key1: 'someValue', key2: 'anotherValue'}}
The componentName
is a property assigned to each component. For example, the Name
component has a default prop set as name
. Then, each component decides how the key/value pairs are organized. Again, in our Name
component, we have two input fields: first_name
and last_name
. When the input field controlling the first_name
changes, the Name
component calls:
super.handleChange("first_name", e.target.value);
This assigns the value from the input field (e.target.value
) to the key first_name
. So the full state looks like:
{
name: {
'first_name': 'First'
}
}
The last_name
value is handled in the same way, except the key passed is last_name
. Other components only have one value, and they all pass their value up with the key value
.
This is also important for how you pass state down to the individual components via props
. Each component consumes values in exactly the same format that it raises them.
As you can see in our original example, the PaymentForm
takes a collection of objects in as it's properties. Each object is state information for each sub-object (and in some cases, sub-sub-objects) that make up the PaymentForm
.
You must include all of the different properties as shown in the example above. We lift all state up, so if you do not pass any properties back down into the PaymentForm
none of the user's input will display.
This might seem cumbersome at first, but this allows your component that is renndering the PaymentForm
to have complete insight into the values inside the form. You can then pick and choose what values you chose to send to your backend, and how they are formatted.
The Base
component (and therefore, by extension, every component) has an onChange
property. Use this property to pass a function to the child component that will manage state changes. Typically, this is just the Base
object's handleChange
function. Some functions, such as the PostalCode
component, need to do extra formatting on the input data before passing it up, so the handleChange
function can be overriden.
This next section will lay out all of the different properties for each component. All propTypes
and defaultProps
included in the Base
element are preserved in every element that extends it.
You have to be careful when preserving these though. propTypes
are inherited, but they are exteremly easy to override. You should do something like the following:
Name.propTypes = {
value: PropTypes.string,
... Name.propTypes // include any existing, inherited propTypes
}
Name.defaultProps = {
value: '',
... Name.propTypes //include any existing, inherited defaultProps
}
The Base
object is our basic building block for all other components. All components that extend Base
also have these properties.
By default, we set onChange
to do nothing. This prevents breaking errors when you forget to include an onChange
event.
Base.propTypes = {
onChange: PropTypes.func,
componentName: PropTypes.string.isRequired
};
Base.defaultProps = {
onChange: () => {return;}
};
The PaymentForm
is our default full PaymentForm. It wraps all of the other components.
The important part about PaymentForm
is it is also responsible for tokenizing credit card information using WePay's tokenization library. This requires a WePay client_id. You also have to signal if this on their Production or Stage environment. This is done using the production
boolean.
It also comes with onTokenized
and onError
events. onTokenized
is fired when the card tokenization process is successful. onError
occurs if the tokenization process fails. You can pass functions to these events to handle these events in a way that makes sense for you and your users.
Typically, after onTokenized
you would want to send the token to your backend so that you can use WePay's /checkout/create
endpoint to actually charge the card. For onError
, you will want to provide error messages to your users.
NOTE:
We need to examine how to make error handling universal. It may be that PaymentForm
can provide a utility function for handling common WePay errors and inserting helpful tooltips to tell the user what to fix.
PaymentForm.propTypes = {
client_id: PropTypes.string.isRequired,
production: PropTypes.bool,
name: PropTypes.object,
email: PropTypes.object,
onTokenized: PropTypes.func,
onError: PropTypes.func,
payment_info: PropTypes.object,
amount:PropTypes.object,
...PaymentForm.propTypes
};
PaymentForm.defaultProps = {
production: false,
componentName: 'payment_form',
payment_info: {},
name: { first_name: '', last_name: '' },
email: { email: '' },
amount: {},
onError: (err) => {console.log(err);},
...PaymentForm.defaultProps
};
The Amount
component manages the amount the payer is paying. You can allow the user to enter an amount in this field (for a Crowdfunding site for example) or make it static (for an e-commerce site for example).
It has built in validation to make sure any user input is actually a number.
Amount.propTypes = {
currency: PropTypes.string.isRequired,
value: PropTypes.string,
editable: PropTypes.bool,
...Amount.propTypes
};
Amount.defaultProps = {
currency: 'USD',
value: '',
componentName: 'amount',
editable: false,
...Amount.defaultProps
};
This component manages email input. It has built in validation to confirm that the user input is actually an email.
Email.propTypes = {
componentName: PropTypes.string.isRequired,
value: PropTypes.string,
...Email.propTypes
};
Email.defaultProps = {
componentName: 'email',
value:'',
...Email.defaultProps
};
The Name
component is really two input fields, one for first_name
and one for last_name
.
If you want to try and merge this into one field, please make sure to raise the value as an object with both first_name
and last_name
keys.
Name.propTypes = {
first_name: PropTypes.string,
last_name: PropTypes.string,
...Name.propTypes
};
Name.defaultProps = {
componentName: 'name',
first_name: '',
last_name: '',
...Name.defaultProps
};
The PaymentInfo
component is a collection of more sub-components. It is responsible for collecting all fields that are tied to the actual credit card, such as the card number itself, the billing address, CVC, and expiration date.
Most of these components have special formatting and validation that is handled by the payment library.
PaymentInfo.defaultProps = {
componentName: 'payment_info',
address: {
billing_address: undefined,
city: undefined,
country: undefined,
postal_code: undefined,
state:undefined
},
card_number: { value: '' },
cvc: { value: '' },
expiration: { value: '' },
...PaymentInfo.defaultProps
};
Collects and validates card number. By "validate" we mean we check to verify that the number could be valid. The actually validity of the card cannot be tested until you call WePays /checkout/create
endpoint.
CardNumber.defaultProps = {
value:PropTypes.string,
...CardNumber.defaultProps
};
CardNumber.defaultProps = {
componentName: 'card_number',
value: '',
...CardNumber.defaultProps
};
The CVC is also referred to as CVV or CVV2.
CVC.propTypes.value = PropTypes.string;
CVC.defaultProps = {
componentName: 'cvc',
value: '',
...CVC.defaultProps
};
The expiration month and year for the card. This is collected in a single input box.
Expiration.propTypes = {
value: PropTypes.string,
...Expiration.propTypes
};
Expiration.defaultProps = {
componentName: 'expiration',
value: '',
...Expiration.defaultProps
};
The Address
component has several sub-components. Some of the values are extremely advised although, not required, if the payer is based out of the US.
Address.propTypes = {
country: PropTypes.object,
billing_address: PropTypes.object,
postal_code: PropTypes.object,
state: PropTypes.object,
...Address.propTypes
};
Address.defaultProps = {
componentName: 'address',
country: { value: 'US' },
billing_address: { value: '' },
postal_code: { value: '' },
state: { value: '' },
...Address.defaultProps
};
This is the street address of the payer. Strongly recommended that this information be collected if the user is based outside of the US.
BillingAddress.propTypes = {
value:PropTypes.string,
...BillingAddress.propTypes
};
BillingAddress.defaultProps = {
componentName: 'billing_address',
value: '',
...BillingAddress.defaultProps
};
The city that the payer is based in. Strongly recommended that this information be collected if the user is based outside of the US.
City.propTypes = {
value: PropTypes.string,
country: PropTypes.string
};
City.defaultProps = {
componentName: 'city',
value: '',
country: 'US',
...City.defaultProps
};
The country that the payer is based in. This is required for all payers.
Country.propTypes ={
value: PropTypes.string,
...Country.propTypess
};
Country.defaultProps = {
componentName: 'country',
value: 'US',
...Country.defaultProps
};
The postal code (or zip-code) that the payer is based in. This is required for all payers.
PostalCode.propTypes = {
country: PropTypes.string,
value: PropTypes.string,
...PostalCode.propTypes
};
PostalCode.defaultProps = {
country: 'US',
value: '',
componentName: 'postal_code',
...PostalCode.defaultProps
};
The state/region that the payer is based in.
State.propTypes = {
country: PropTypes.string,
value: PropTypes.string,
...State.propTypes
};
State.defaultProps = {
country: 'US',
componentName: 'state',
value: State.US_STATES[0],
...State.defaultProps
};
The default form may not look the way you want it to, but this architecture was built with the idea that you can modify the cosmetic elements, but keep the underlying data flow the same.
PaymentForm
actually has references to all of it's children component classes, and each of those children component classes has references to their children. This means all you have to do is import PaymentForm
and extend it's render function.
For example, if we want to move the Amount
field to the top of the screen instead of the bottom:
// using react-bootstrap
import React, { Component } from 'react';
import {PaymentForm} from 'wepay-react-checkout';
import { Grid, FormGroup, Col, Button } from 'react-bootstrap';
import './App.css';
class PaymentForm2 extends PaymentForm {
render() {
const payment_info = this.props.payment_info ? this.props.payment_info : {};
const name = this.props.name ? this.props.name : {};
const email = this.props.email ? this.props.email : {};
const amount = this.props.amount ? this.props.amount: {};
// move the amount field up top
return (<div className="App">
<Grid>
<form
className="form-horizontal"
onSubmit={this.handleSubmit}>
<FormGroup>
<Col lg={12}>
<PaymentForm.Amount
onChange={this.handleChange}
{...amount}
/>
</Col>
</FormGroup>
<FormGroup>
<Col lg={12}>
<PaymentForm.Name
onChange={this.handleChange}
{...name} />
</Col>
</FormGroup>
<Col lg={12}>
<PaymentForm.Email
onChange={this.handleChange}
{...email} />
</Col>
<FormGroup>
<Col lg={12}>
<PaymentForm.PaymentInfo
onChange={this.handleChange}
{...payment_info}
/>
</Col>
</FormGroup>
<FormGroup>
<Col lg={12}>
<Button
type="submit"
bsStyle="success"
block>
Pay
</Button>
</Col>
</FormGroup>
</form>
</Grid>
</div>)
}
}
class App extends Component {
constructor(props) {
super(props);
this.state = {
payment_form: {
payment_info: {},
name: {},
email: {},
}
};
this.handleChange = this.handleChange.bind(this);
this.handleTokenized = this.handleTokenized.bind(this);
}
handleChange(name, value) {
var new_state = this.state;
new_state[name] = value;
this.setState(new_state);
}
// on success occurs when the card is tokenized
// at this point, you need to send the tokenized card to your backend server
// along with the payment information (such as cost)
handleTokenized(response) {
// we are going to re-package the info
// this is all we need to call WePay's /checkout/create
var payload = {
credit_card_id:response.credit_card_id,
amount: this.state.payment_form.amount.value,
currency: this.state.payment_form.amount.currency,
};
alert(`Submit to backend: ${JSON.stringify(payload)}`);
}
render() {
const payment_form = this.state.payment_form ? this.state.payment_form : {};
// this remains the same except its a different object name
return(
<PaymentForm2
client_id="YOUR APP ID"
production={false}
onChange={this.handleChange}
onTokenized={this.handleTokenized}
name={payment_form.name}
email={payment_form.email}
amount={payment_form.amount}
payment_info={payment_form.payment_info}
/>
)
}
}
export default App;
This can be repeated to modify the entire checkout form, but keep the data pipeline the same. Simply override the render
method and make sure to pass the appropriate props down to the right components.
It may be that you want to build a different template than you can extend and override the render methods for all components. You can also add new components that extend Base
and follow the same data flow rules.
You should create a directory under components
with the name of your template. You can then import PaymentForm
from the PaymentForm
directory and extend each component and override the render
method.
- Make setting
required
on components a property instead of hardcoded - Improve validation on components
- Create new templates