PayPal API interfaces for React Webapps - Buttons and Fees
MartinJ
Posted on March 24, 2024
Last reviewed: March 2024
Bridget Riley : Bolt of Colour, 2017-2019. (fragment)
Background
PayPal is a really useful tool for developing payment-collection arrangements in a javascript webapp. In the past I've only used its php interface, but when I tried out its React API recently I was impressed to see how much simpler things had become. Here's a brief description of the payment collection bit, together with a deeper dive into my experiences using my PayPal "secret key" to access PayPal fee information. I use this to generate Fee reports for Management.
Once you've registered a PayPal development a/c (see Solid Help Center's useful guide if you've not done this before), you'll find you have two pairs of access keys - one pair for Paypal's Sandbox and the other for Live operation. The first key in each pair just identifies your site, so can be used safely in client code to collect payments (anybody who wants to use my code to push payments into my a/c, is welcome to do so!). When you're working with the Sandbox key, everything works just like it will with the Live key, but the "customers" and their card details are all imaginary. This means you can test every detail of your PayPal integration in every detail without spending any actual cash.
1. A simple PayPal button
Here's the code for a simple PayPal payment collection page in a React SPA. I know that it looks rather too long to be called "simple" by any reasonable person, but it's mostly comment. There are a few wrinkles that can cause trouble, but the code is really pretty straightforward once you've got its patterns clear in your head.
import React, { useState, useEffect } from 'react';
import ReactDOM from "react-dom"
// Load the Paypal library
import { loadScript } from "@paypal/paypal-js"; // install this with npm install @paypal/paypal-js
function PaypalDemoButton() {
// define a React "screen state" arrangement for the component
const [screenState, setScreenState] = useState({})
// Here's the tricky bit. The code is aiming to create a button that displays
// a PayPal logo (you can configure the button's appearance to your particular requirements
// but I'm going to use a default here). When clicked, this displays a popup that enables PayPal
// to negotiates with a customer and extract a specified amount of cash from their credit account.
// Once this concludes, PayPal returns data about your customer's order via an "onApprove" event
// function that you've supplied.
// The "SellItemButton" button is created on a "PayPal" object drawn from loadScript. But this is
// an asynch process that needs to be handled with care. Here's my own solution to the problem.
var SellItemButton // Declare the button we're aiming to create
// Define a useEffect to manage the creation of the "PayPal" instance you'll need for the button.
// Inside this, first define and then launch an async function to await the construction of a PayPal
// object. Once this resolves, create your PayPal button. Now, as you'll know, a useEffect runs **after**
// the initial render of its parent component so (1) you'll need to find a way of only "returning" your
// jsx when its contents are defined and (2) when the useEffect **does** run, you'll need to launch a
// React "state change" call to trigger a re-render. Both of these requirements can be met by using a
// setScreenState to put copies of the local PayPal and SellItemButton variables into state. Because I've
// not declared any dependencies for the useEffect (by concluding with a ", []);"), this will run once
// and once only
useEffect(() => {
async function initialisePaypal() {
try {
// Instrument a paypal variable with parameters for your specific project - here, client-id is
// a sandbox key. The "vault: true" setting below makes the credit/debit card bit of the Paypal
// interaction open in its own window, like the Paypal membership interaction itself - see
// https://github.com/paypal/paypal-checkout-components/issues/1467
let paypal = await loadScript({
"client-id": "ASEKu7Olgf82tOA ... obfuscated Sandbox client key ...",
"currency": "GBP",
vault: true });
let SellItemButton = paypal.Buttons.driver("react", { React, ReactDOM });
// once the next command fires, the screen will rerender and display your button
setScreenState({...screenState, paypal: paypal, SellItemButton: SellItemButton})
} catch (error) {
window.alert("failed to load the PayPal JS SDK script", error);
}
}
initialisePaypal()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Define a PayPal "createOrder" function to tell PayPal what to do when the button is clicked. This fixed
// demo version tells PayPal to charge the customer £10 . Obviously, in practice, you'd probably want to
// generate amounts dynamically.
const sellItemViaPaypal = (data, actions) => {
return actions.order.create({
purchase_units: [{ amount: {value: 10.00 }}]
})
};
// Create a Paypal "onApprove" function for PayPal to launch once the user has paid. This one uses
// the customer details object returned by PayPal to recover customer names and address details.
const onSellItemViaPaypalApprove = (data, actions) => {
// return purchase details using actions.order.capture - see https://developer.paypal.com/sdk/js/reference/
return actions.order.capture().then(function (details) {
// Use the details field returned by actions.order.capture() to organise delivery. The PayPal payer
// object is defined at https://developer.paypal.com/docs/api/orders/v2/#definition-payer
const customerObject = {
givenName: details.payer.name.given_name,
surnames: details.payer.name.surname,
}
console.log ("Yippee - payment received from " + customerObject.givenName)
})
}
// Suppress display of the initial render (because at this point both paypal and the paypal button
// are still undefined)
if (screenState.paypal) return (
<div>
{(screenState.paypal) &&
<div style= {{width: "20%", margin: "auto"}}>
<h2> Demo PayPal button </h2>
<screenState.SellItemButton
createOrder={(data, actions) => sellItemViaPaypal(data, actions)}
onApprove={(data, actions) => onSellItemViaPaypalApprove(data, actions)
}
/>
</div>
}
</div>
)
}
export { PaypalDemoButton }
If you launch this component in a React program you should see the following on your screen
Click the button and you should see a PayPal popup appear to allow you to enter user details registered in your sandbox a/c. Once PayPal has digested these, if you use your browser's inspection tool to view your console you should find that this has logged a "Yippee" message for your test customer.
2. Digging deeper
For most purposes, variants on the above are all you need. But supposing you wanted to collect information about transaction details. In my own case, I wanted to capture details of the fees that PayPal charges. You may be surprised to find that though these are displayed in the PayPal activity records you see when you log into the PayPal site, they don't seem to be available to a webapp as fields in actions.order.capture()
. Still, all is not lost. Remember that when you registered with PayPal you got a "secret key"? You can use this to dive into PayPal's records and retrieve the data.
But you'll encounter two problems.
In the first place, PayPal is insistent that your systems must ensure that your secret keys stay secret. This seems entirely reasonable. You can probably do some serious IT with a secret PayPal key and I wouldn't like to think that someone might convert my PayPal balance into crypto dollars and move it into their wallet!
Secondly, you'll quickly discover that Paypal records are seriously complicated. Finding fee information within them took me quite some time. But everything is well-documented so, whatever you might be looking for, I'm pretty sure you'll find it - just be prepared for a concentrated bout of docs research.
Anyway, the need to keep your key secret means that you'll have to code this type of Payal activity server-side. As a Firebase user this means that I had to use a firebase function. My keys here are secured by my Google account's password.
The first thing I need to do in my function is to log into the PayPal site with my key pair as a "username, password" combo and get an auth token
In the past, being a client-side javascript coder, I'd have used the Fetch function to poke the PayPal url. But Fetch isn't available in the Node.js environment of a Cloud function so, in the example below, I use Axios to get my auth token.
Once over this hurdle I can then use my token, together with the paymentID for the transaction I'm interested in, to poke the PayPal url via a second Axios call . A curious thing here is that this paymentID is a completely different key from the transactionId you'll see detailed in your transaction activity report on PayPal. Why this should be is another mystery, but it's not a problem because paymentID is available as a field in the details field returned by actions.order.capture()
.
As commented earlier, the feesDetail object returned by access to the PayPal url is immensely complicated. I found my fees at feeDetails.data.purchase_units[0].payments.captures[0].seller_receivable_breakdown.paypal_fee.value
.
So, here's the code for a Google Cloud function to retrieve PayPal fee information. I've coded this as an onCall function because I find that this handles both input and output smoothly and doesn't create CORS issues. My functions are always parameterised to run in the UK, but you'd obviously want to consider changing this to suit your own particular situation.
exports.getPaypalFeeForId = functions.region('europe-west2').https.onCall((dataJSON) => {
// Retrieve paramters packed inside a JSdataJSON input field:
// runMode - "test_"/"live_" toggles between sandbox and live PayPal accounts
// paymentId - the id of the transaction you're targetting
const dataObject = JSON.parse(dataJSON);
// declare a tracker to help the calling program identify the location of any errors that may be returned
let errorLocation = "Token request";
// use runMode to initialise the approprate PayPal url targets. My PayPal keys are tucked away in my
// project's functions.config().config. object
const username = (dataObject.runMode === "test_") ? functions.config().config.paypaltestclientid : functions.config().config.paypalliveclientid;
const password = (dataObject.runMode === "test_") ? functions.config().config.paypaltestclientsecret : functions.config().config.paypalliveclientsecret;
const tokenUrl = (dataObject.runMode === "test_") ? 'https://api.sandbox.paypal.com/v1/oauth2/token' : 'https://api.paypal.com/v1/oauth2/token';
const feeDetailsUrl = (dataObject.runMode === "test_") ? 'https://api.sandbox.paypal.com/v2/checkout/orders/' : 'https://api.paypal.com/v2/checkout/orders/';
// Build a Promise to return the result
return new Promise(async function (resolve, reject) {
try {
// launch axios, installed in your project with "npm install axios" and declared
// with "const axios = require('axios');"
const token = await axios.post(tokenUrl, 'grant_type=client_credentials', {
headers: {
'Accept': 'application/json',
'Accept-Language': 'en_US',
'Content-Type': 'application/x-www-form-urlencoded',
'Access-Control-Allow-Origin': '*',
},
auth: {
username: username,
password: password
}
});
// You're now logged in so use the axios equivalent of the following example:
// curl - v - X POST "https://api.sandbox.paypal.com/v2/checkout/orders/8AA28...."
// to get your payment data for paymentId 8AA28.... (Note that Sandbox and Live
// transactions are at different addresses)
errorLocation = "Fee Detail request ";
const feeDetails = await axios.get(feeDetailsUrl + dataObject.paymentId, {
headers: { Authorization: `Bearer ${token.data.access_token}` }
})
return { fee: feeDetails.data.purchase_units[0].payments.captures[0].seller_receivable_breakdown.paypal_fee.value };
} catch (error) {
console.log(" error = " + error)
return { error: error, errorLocation: errorLocation }
}
})
})
This might be called in the following way from client-side code to recover the PayPal Fee for a given paymentId
.
import { getFunctions, connectFunctionsEmulator } from 'firebase/functions';
import { httpsCallable } from 'firebase/functions';
const firebaseConfig = {
firebase_config_keys: "obfuscated ... the usual stuff"
};
const app = initializeApp(firebaseConfig);
const functions = getFunctions(app, 'europe-west2');
const getPaypalFeeForId = httpsCallable(functions, 'getPaypalFeeForId');
const dataObject = {
runMode: "test_"",
paymentId: "4FM338 ... obfuscated id"
};
const dataJSON = JSON.stringify(dataObject);
try {
const feeData = await getPaypalFeeForId(dataJSON)
console.log("Success ", feeData.data.fee; // Here's your fee!
} catch (error) {
console.log("Failure: error details are : ", error)
}
Thanks for reading this. I hope you find it useful
Posted on March 24, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
April 22, 2023