Copyright Derek O'Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.
In order to use PayPal, you need to set up a PayPal developer account. You can do this at the link https://developer.paypal.com/developer/accounts/
After you have created an account and logged-in, you need to go to the "My Apps & Credentials" menu option, then select "Sandbox" and then "Create App". In the screenshot below, I have created an app called "Full Stack Development".
When you click into your app, you should see the screen shown below. From here, you can obtain your Sandbox Client ID. The Sandbox Client ID will allow you to test your code without using real-world money. You will need to use your Sandbox Client ID in the example code that we develop during this section of the notes.
Store the Sandbox Client ID that you get from PayPal as a constant in the client/config/global_constants.js file, as shown below. You need to replace the red text below with your own Sandbox Client ID.
If you wish to create a real-world system that takes real payments, then you will need to provide your Production Client ID, too. For testing purposes, we do not need to change this string.
... // PayPal
export const SANDBOX_CLIENT_ID = "REPLACE_THIS_TEXT_WITH_YOUR_SANDBOX_CLIENT_ID"
export const PRODUCTION_CLIENT_ID = "TO_MAKE_A_LIVE_REAL_WORLD_APPLICATION_REPLACE_THIS_TEXT_WITH_YOUR_PRODUCTION_CLIENT_ID" ...
Clicking on the "Credit Card Generator" menu item will allow you to generate a test credit card. You only need to create one test credit card. You can use this test credit card for all of the testing that you do within your application.
The "@paypal/react-paypal-js" package can be used to display a PayPal checkout button in our web application. The button will look like the one in the image below:
Clicking the checkout button will call the PayPal checkout dialog, as shown below. When we use the sandbox Client ID, PayPal opens in test mode. This means that no real-world money is taken for purchases.
Install the package "@paypal/react-paypal-js"
We need to embed the PayPal checkout button within our own custom component, so that we can encapsulate the user's interaction with the PayPal checkout dialog. In the code below, we create a component called BuyCar. This component can be called from within the render() code in the client/src/components/CarTableRow.js file using the code below. In the code below, we also pass the car's price (in the example below it is €30,000) to the BuyCar component, so that it can be shown in the PayPal checkout dialog, as shown in the image below.
<BuyCar price={this.props.car.price}/>
The '@paypal/react-paypal-js' package contains a component that is called PayPalScriptProvider. This component wraps around a PaypalButtons component, as shown in the code below. The BuyCar component wraps the PayPalScriptProvider and PaypalButtons components. This allow us to tailor the PaypalButtons for our own application. A minimal version of the BuyCar component is shown in the code below. The code will cause the PaypalButtons to use the car's price that was passed as a prop when we created the BuyCar component.
import React, {Component} from "react" import axios from "axios" import {Redirect} from "react-router-dom" import {SANDBOX_CLIENT_ID, SERVER_HOST} from "../config/global_constants" import PayPalMessage from "./PayPalMessage" import {PayPalButtons, PayPalScriptProvider} from "@paypal/react-paypal-js" export default class BuyCar extends Component { createOrder = (data, actions) => { return actions.order.create({purchase_units:[{amount:{value:this.props.price}}]}) } onApprove = paymentData => { console.log("PayPal payment successful") } onError = errorData => { console.log("PayPal payment error") } onCancel = cancelData => { console.log("PayPal payment cancelled") } render() { return ( <div> <PayPalScriptProvider options={{currency:"EUR", "client-id":SANDBOX_CLIENT_ID }}> <PayPalButtons style={{layout: "horizontal"}} createOrder={this.createOrder} onApprove={this.onApprove} onError={this.onError} onCancel={this.onCancel}/> </PayPalScriptProvider> </div> )} }
Open the error_handling project from the previous section in these notes. Add the BuyCar component to your application. Change the code to implement PayPal purchases using the BuyCar component, as shown below:
Develop a PayPalMessage component that can be used to provide the user with success, cancel and error messages, as shown below:
Improve the code above, so that it saves the sales details to a "sales" document on the server-side to record successful sales. The "sales" document should include:
Add a Boolean property (which you should call sold) to the "cars" collection to record if a car has been sold. Use the sold property to display either the PayPal checkout button or the word "SOLD" for each car, as shown below:
The full project code for the "Cars" Worked Example that is described below can be downloaded from this link.
In this example, we implement a PayPal checkout button. We also provide user feedback after the PayPal transaction has been completed. We save successful sales details to the database.
// This file holds global constants that are visible on the Client-side // Access level export const ACCESS_LEVEL_GUEST = 0 export const ACCESS_LEVEL_NORMAL_USER = 1 export const ACCESS_LEVEL_ADMIN = 2 // PayPal export const SANDBOX_CLIENT_ID = "REPLACE_THIS_TEXT_WITH_YOUR_SANDBOX_CLIENT_ID"
export const PRODUCTION_CLIENT_ID = "TO_MAKE_A_LIVE_REAL_WORLD_APPLICATION_REPLACE_THIS_TEXT_WITH_YOUR_PRODUCTION_CLIENT_ID" // Server export const SERVER_HOST = `http://localhost:4000`
You must replace SANDBOX_CLIENT_ID with your own Sandbox Client ID, so that you can run PayPal in test mode.
You do not have to change the PRODUCTION_CLIENT_ID. This is only needed when we make real-world applications.
import React, {Component} from "react" import axios from "axios" import {Redirect} from "react-router-dom" import {SANDBOX_CLIENT_ID, SERVER_HOST} from "../config/global_constants" import PayPalMessage from "./PayPalMessage" import {PayPalButtons, PayPalScriptProvider} from "@paypal/react-paypal-js" export default class BuyCar extends Component { constructor(props) { super(props) this.state = {redirectToPayPalMessage:false, payPalMessageType:null, payPalOrderID:null} } createOrder = (data, actions) => { return actions.order.create({purchase_units:[{amount:{value:this.props.price}}]}) } onApprove = paymentData => { axios.post(`${SERVER_HOST}/sales/${paymentData.orderID}/${this.props.carID}/${this.props.price}`, {headers:{"authorization":localStorage.token, "Content-type": "multipart/form-data"}}) .then(res => { this.setState({payPalMessageType:PayPalMessage.messageType.SUCCESS, payPalOrderID:paymentData.orderID, redirectToPayPalMessage:true}) }) .catch(errorData => { this.setState({payPalMessageType:PayPalMessage.messageType.ERROR, redirectToPayPalMessage:true}) }) } onError = errorData => { this.setState({payPalMessageType:PayPalMessage.messageType.ERROR, redirectToPayPalMessage:true}) } onCancel = cancelData => { // The user pressed the Paypal checkout popup window cancel button or closed the Paypal checkout popup window this.setState({payPalMessageType:PayPalMessage.messageType.CANCEL, redirectToPayPalMessage:true}) } render() { return ( <div> {this.state.redirectToPayPalMessage ? <Redirect to= {`/PayPalMessage/${this.state.payPalMessageType}/${this.state.payPalOrderID}`}/> : null} <PayPalScriptProvider options={{currency:"EUR", "client-id":SANDBOX_CLIENT_ID }}> <PayPalButtons style={{layout: "horizontal"}} createOrder={this.createOrder} onApprove={this.onApprove} onError={this.onError} onCancel={this.onCancel}/> </PayPalScriptProvider> </div> )} }
The onSuccess, onCancel and onError methods set the PayPalMessageType that will be passed to the PayPalMessage component.
In addition, the onSuccess method uses the axios() method to pass the payment ID, car ID, recipient name and recipient email to the server-side, so that the sale can be recorded in the database.
onSuccess = paymentData => { axios.post(`${SERVER_HOST}/sales/${paymentData.paymentID}/${this.props.carID}/${this.props.price}/${paymentData.address.recipient_name}/${paymentData.email}`, {headers:{"authorization":localStorage.token, "Content-type": "multipart/form-data"}}) .then(res => { this.setState({payPalMessageType:PayPalMessage.messageType.SUCCESS, payPalPaymentID:paymentData.paymentID, redirectToPayPalMessage:true}) }) .catch(errorData => { console.log("PayPal payment unsuccessful error:", errorData) this.setState({payPalMessageType:PayPalMessage.messageType.ERROR, redirectToPayPalMessage:true}) }) }
import React, {Component} from "react" import {Redirect, Link} from "react-router-dom" export default class PayPalMessage extends Component { static messageType = {SUCCESS:"success", ERROR:"error", CANCEL:"cancel"} constructor(props) { super(props) this.state = {redirectToDisplayAllCars:false, buttonColour:"red-button"} } componentDidMount() { if(this.props.match.params.messageType === PayPalMessage.messageType.SUCCESS) { this.setState({heading:"PayPal Transaction Confirmation", message:"Your PayPal transaction was successful.", buttonColour:"green-button"}) } else if(this.props.match.params.messageType === PayPalMessage.messageType.CANCEL) { this.setState({heading:"PayPal Transaction Cancelled", message:"You cancelled your PayPal transaction. Therefore, the transaction was not completed."}) } else if(this.props.match.params.messageType === PayPalMessage.messageType.ERROR) { this.setState({heading:"PayPal Transaction Error", message:"An error occured when trying to perform your PayPal transaction. The transaction was not completed. Please try to perform your transaction again."}) } else { console.log("The 'messageType' prop that was passed into the PayPalMessage component is invalid. It must be one of the following: PayPalMessage.messageType.SUCCESS, PayPalMessage.messageType.CANCEL or PayPalMessage.messageType.ERROR") } } render() { return ( <div className="payPalMessage"> {this.state.redirectToDisplayAllCars ? <Redirect to="/DisplayAllCars"/> : null} <h3>{this.state.heading}</h3> <p>{this.props.match.params.message}</p> <p>{this.state.message}</p> {this.props.match.params.messageType === PayPalMessage.messageType.SUCCESS ? <p>Your PayPal payment confirmation is <span id="payPalPaymentID">{this.props.match.params.payPalPaymentID}</span></p> : null} <p id="payPalPaymentIDButton"><Link className={this.state.buttonColour} to={"/DisplayAllCars"}>Continue</Link></p> </div> ) } }
The messageType can be SUCCESS, ERROR or CANCEL.
static messageType = {SUCCESS:"success", ERROR:"error", CANCEL:"cancel"}
The messageType will determine the content of the message that is displayed.
componentDidMount()
{
if(this.props.match.params.messageType === PayPalMessage.messageType.SUCCESS)
{
this.setState({heading:"PayPal Transaction Confirmation",
message:"Your PayPal transaction was successful.",
buttonColour:"green-button"})
}
else if(this.props.match.params.messageType === PayPalMessage.messageType.CANCEL)
{
this.setState({heading:"PayPal Transaction Cancelled",
message:"You cancelled your PayPal transaction. Therefore, the transaction was not completed."})
}
else if(this.props.match.params.messageType === PayPalMessage.messageType.ERROR)
{
this.setState({heading:"PayPal Transaction Error",
message:"An error occured when trying to perform your PayPal transaction. The transaction was not completed. Please try to perform your transaction again."})
}
else
{
console.log("The 'messageType' prop that was passed into the PayPalMessage component is invalid. It must be one of the following: PayPalMessage.messageType.SUCCESS, PayPalMessage.messageType.CANCEL or PayPalMessage.messageType.ERROR")
}
}
The default button colour is set to red. It is changed to green if the PayPal transaction is successful.
constructor(props)
{
super(props)
this.state = {redirectToDisplayAllCars:false,
buttonColour:"red-button"}
}
componentDidMount() { if(this.props.match.params.messageType === PayPalMessage.messageType.SUCCESS) { this.setState({heading:"PayPal Transaction Confirmation", message:"Your PayPal transaction was successful.", buttonColour:"green-button"}) } ... }
import React, {Component} from "react" import {BrowserRouter, Switch, Route} from "react-router-dom" import "bootstrap/dist/css/bootstrap.css" import "./css/App.css" import Register from "./components/Register" import ResetDatabase from "./components/ResetDatabase" import Login from "./components/Login" import Logout from "./components/Logout" import AddCar from "./components/AddCar" import EditCar from "./components/EditCar" import DeleteCar from "./components/DeleteCar" import DisplayAllCars from "./components/DisplayAllCars" import LoggedInRoute from "./components/LoggedInRoute" import BuyCar from "./components/BuyCar" import PayPalMessage from "./components/PayPalMessage" import {ACCESS_LEVEL_GUEST} from "./config/global_constants" if (typeof localStorage.accessLevel === "undefined") { localStorage.name = "GUEST" localStorage.accessLevel = ACCESS_LEVEL_GUEST localStorage.token = null localStorage.profilePhoto = null } export default class App extends Component { render() { return ( <BrowserRouter> <Switch> <Route exact path="/Register" component={Register} /> <Route exact path="/ResetDatabase" component={ResetDatabase} /> <Route exact path="/" component={DisplayAllCars} /> <Route exact path="/Login" component={Login} /> <Route exact path="/BuyCar/:id" component={BuyCar} /> <Route exact path="/PayPalMessage/:messageType/:payPalPaymentID" component={PayPalMessage}/> <LoggedInRoute exact path="/Logout" component={Logout} /> <LoggedInRoute exact path="/AddCar" component={AddCar} /> <LoggedInRoute exact path="/EditCar/:id" component={EditCar} /> <LoggedInRoute exact path="/DeleteCar/:id" component={DeleteCar} /> <Route exact path="/DisplayAllCars" component={DisplayAllCars}/> <Route path="*" component={DisplayAllCars}/> </Switch> </BrowserRouter> ) } }
Add the BuyCar and PayPalMessage components to the app.
import React, {Component} from "react" import {Link} from "react-router-dom" import axios from "axios" import {ACCESS_LEVEL_GUEST, ACCESS_LEVEL_ADMIN, SERVER_HOST} from "../config/global_constants" import BuyCar from "./BuyCar" export default class CarTableRow extends Component { componentDidMount() { this.props.car.photos.map(photo => { return axios.get(`${SERVER_HOST}/cars/photo/${photo.filename}`) .then(res => { document.getElementById(photo._id).src = `data:;base64,${res.data.image}` }) .catch(err => { // do nothing }) }) } render() { let soldOrForSale = null if(localStorage.accessLevel <= ACCESS_LEVEL_GUEST) { if(this.props.car.sold !== true) { soldOrForSale = <BuyCar carID={this.props.car._id} price={this.props.car.price} /> } else { soldOrForSale = "SOLD" } } return ( <tr> <td>{this.props.car.model}</td> <td>{this.props.car.colour}</td> <td>{this.props.car.year}</td> <td>{this.props.car.price}</td> <td className="carPhotos"> {this.props.car.photos.map(photo => <img key={photo._id} id={photo._id} alt=""/>)} </td> <td> {localStorage.accessLevel > ACCESS_LEVEL_GUEST ? <Link className="green-button" to={"/EditCar/" + this.props.car._id}>Edit</Link> : null} {localStorage.accessLevel >= ACCESS_LEVEL_ADMIN ? <Link className="red-button" to={"/DeleteCar/" + this.props.car._id}>Delete</Link> : null} {soldOrForSale} </td> </tr> ) } }
When a car is sold, we set its sold property to true in the cars document. Note that the sold property is new to our cars collection. We shall see how it is set in the server-side code that follows at the bottom of this section of the notes.
If this.props.car.sold !== true, it means that the car has not been sold and the BuyCar component (i.e. a PayPal button) needs to be displayed.
soldOrForSale is used to display either a BuyCar component (i.e. a PayPal button) or the word "SOLD".
let soldOrForSale = null if(localStorage.accessLevel <= ACCESS_LEVEL_GUEST) { if(this.props.car.sold !== true) { soldOrForSale = <BuyCar carID={this.props.car._id} price={this.props.car.price} /> } else { soldOrForSale = "SOLD" } } return ( <tr> <td>{this.props.car.model}</td> <td>{this.props.car.colour}</td> <td>{this.props.car.year}</td> <td>{this.props.car.price}</td> <td className="carPhotos"> {this.props.car.photos.map(photo => <img key={photo._id} id={photo._id} alt=""/>)} </td> <td> {localStorage.accessLevel > ACCESS_LEVEL_GUEST ? <Link className="green-button" to={"/EditCar/" + this.props.car._id}>Edit</Link> : null} {localStorage.accessLevel >= ACCESS_LEVEL_ADMIN ? <Link className="red-button" to={"/DeleteCar/" + this.props.car._id}>Delete</Link> : null} {soldOrForSale} </td> </tr> )}
// Server-side global variables
require(`dotenv`).config({path:`./config/.env`})
// Database
require(`./config/db`)
// Express
const express = require(`express`)
const app = express()
app.use(require(`body-parser`).json())
app.use(require(`cors`)({credentials: true, origin: process.env.LOCAL_HOST}))
//app.all("*", function(req, res, next) {
// res.header("Access-Control-Allow-Origin", "*")
// res.header("Access-Control-Allow-Headers", "X-Requested-With")
// }
// Routers
app.use(require(`./routes/cars`))
app.use(require(`./routes/users`))
app.use(require(`./routes/sales`))
// Port
app.listen(process.env.SERVER_PORT, () =>
{
console.log(`Connected to port ` + process.env.SERVER_PORT)
})
// Error 404
app.use((req, res, next) => {next(createError(404))})
// Handle errors
app.use(function (err, req, res, next)
{
if (!err.statusCode)
{
err.statusCode = 500
}
// check that all required paramters are not empty in any route
if (err instanceof ReferenceError)
{
err.statusCode = 400
err.message = "Cannot reference a variable that has not been declared. This can be caused in run-time if the user did not input a parameter that is required by a router"
}
// Server-side error message
console.log(err.message + "\nError Details...")
// Server-side error details
console.log(err)
// return error message that will be displayed on client-side console
res.status(err.statusCode).send(err.message)
})
We need a new collection - called sales - to keep a record of the sales details in our database.
app.use(require(`./routes/sales`))
const mongoose = require(`mongoose`) let salesSchema = new mongoose.Schema( { paypalPaymentID: {type: String, required:true}, carID: {type: String, required:true}, price: {type: Number, required:true}, customerName: {type: String,required:true}, customerEmail: {type: String,required:true} }, { collection: `sales` }) module.exports = mongoose.model(`sales`, salesSchema)
We need to add a model and scheme for the new sales collection.
const mongoose = require(`mongoose`)
let carPhotosSchema = new mongoose.Schema(
{
filename:{type:String}
})
let carsSchema = new mongoose.Schema(
{
model: {type: String, required:true},
colour: {type: String, required:true},
year: {type: Number, required:true},
price: {type: Number, required:true},
photos:[carPhotosSchema],
sold: {type: Boolean, default:false}
},
{
collection: `cars`
})
module.exports = mongoose.model(`cars`, carsSchema)
Add a sold property to the cars model and schema. This will be set to true when a car is sold.
const router = require(`express`).Router() const salesModel = require(`../models/sales`) const carsModel = require(`../models/cars`) const createNewSaleDocument = (req, res, next) => { // Use the PayPal details to create a new sale document let saleDetails = new Object() saleDetails.paypalPaymentID = req.params.paymentID saleDetails.carID = req.params.carID saleDetails.price = req.params.price saleDetails.customerName = req.params.customerName saleDetails.customerEmail = req.params.customerEmail carsModel.findByIdAndUpdate({_id:req.params.carID}, {sold: true}, (err, data) => { if(err) { return next(err) } }) salesModel.create(saleDetails, (err, data) => { if(err) { return next(err) } }) return res.json({success:true}) } // Save a record of each Paypal payment router.post('/sales/:paymentID/:carID/:price/:customerName/:customerEmail', createNewSaleDocument) module.exports = router
The sales router makes changes to both the cars and sales models.
We need to require both models.
const salesModel = require(`../models/sales`) const carsModel = require(`../models/cars`)
In the cars document, we find the the document that has the _id of req.params.carID and we update its sold property to be the value true .
carsModel.findByIdAndUpdate({_id:req.params.carID}, {sold: true}, (err, data) => { if(err) { return next(err) } })
In the sales colection, we create a new document that holds the saleDetails.
// Use the PayPal details to create a new sale document let saleDetails = new Object() saleDetails.paypalPaymentID = req.params.paymentID saleDetails.carID = req.params.carID saleDetails.price = req.params.price saleDetails.customerName = req.params.customerName saleDetails.customerEmail = req.params.customerEmail salesModel.create(saleDetails, (err, data) => { if(err) { return next(err) } })
Copyright Derek O' Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.