Skip to content

Commit bff0f40

Browse files
committed
Add new card-fields checkout flow
1 parent d3c1ada commit bff0f40

File tree

6 files changed

+394
-0
lines changed

6 files changed

+394
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Advanced Integration Example
2+
3+
This folder contains example code for an Advanced PayPal integration using both the JS SDK and Node.js to complete transactions with the PayPal REST API.
4+
5+
## Instructions
6+
7+
1. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET`.
8+
2. Run `npm install`
9+
3. Run `npm start`
10+
4. Open http://localhost:8888
11+
5. Enter the credit card number provided from one of your [sandbox accounts](https://developer.paypal.com/dashboard/accounts) or [generate a new credit card](https://developer.paypal.com/dashboard/creditCardGenerator)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<link rel="stylesheet" type="text/css"
7+
href="https://www.paypalobjects.com/webstatic/en_US/developer/docs/css/cardfields.css" />
8+
<title>PayPal JS SDK Advanced Integration - Checkout Flow</title>
9+
</head>
10+
<body>
11+
<div id="paypal-button-container"></div>
12+
<div id="card-form">
13+
<div id="card-name-field-container"></div>
14+
<div id="card-number-field-container"></div>
15+
<div id="card-expiry-field-container"></div>
16+
<div id="card-cvv-field-container"></div>
17+
<button id="multi-card-field-button" type="button">Pay now with Card</button>
18+
</div>
19+
<p id="result-message"></p>
20+
<!-- Replace the "test" client-id value with your client-id -->
21+
<script src="https://www.paypal.com/sdk/js?components=buttons,card-fields&client-id=test"></script>
22+
<script src="checkout.js"></script>
23+
</body>
24+
</html>
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
async function createOrderCallback() {
2+
try {
3+
const response = await fetch("/api/orders", {
4+
method: "POST",
5+
headers: {
6+
"Content-Type": "application/json",
7+
},
8+
// use the "body" param to optionally pass additional order information
9+
// like product ids and quantities
10+
body: JSON.stringify({
11+
cart: [
12+
{
13+
id: "YOUR_PRODUCT_ID",
14+
quantity: "YOUR_PRODUCT_QUANTITY",
15+
},
16+
],
17+
}),
18+
});
19+
20+
const orderData = await response.json();
21+
22+
if (orderData.id) {
23+
return orderData.id;
24+
} else {
25+
const errorDetail = orderData?.details?.[0];
26+
const errorMessage = errorDetail
27+
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
28+
: JSON.stringify(orderData);
29+
30+
throw new Error(errorMessage);
31+
}
32+
} catch (error) {
33+
console.error(error);
34+
resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
35+
}
36+
}
37+
38+
async function onApproveCallback(data, actions) {
39+
try {
40+
const response = await fetch(`/api/orders/${data.orderID}/capture`, {
41+
method: "POST",
42+
headers: {
43+
"Content-Type": "application/json",
44+
},
45+
});
46+
47+
const orderData = await response.json();
48+
// Three cases to handle:
49+
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
50+
// (2) Other non-recoverable errors -> Show a failure message
51+
// (3) Successful transaction -> Show confirmation or thank you message
52+
53+
const transaction =
54+
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
55+
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
56+
const errorDetail = orderData?.details?.[0];
57+
58+
// this actions.restart() behavior only applies to the Buttons component
59+
if (errorDetail?.issue === "INSTRUMENT_DECLINED" && !data.card && actions) {
60+
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
61+
// recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
62+
return actions.restart();
63+
} else if (
64+
errorDetail ||
65+
!transaction ||
66+
transaction.status === "DECLINED"
67+
) {
68+
// (2) Other non-recoverable errors -> Show a failure message
69+
let errorMessage;
70+
if (transaction) {
71+
errorMessage = `Transaction ${transaction.status}: ${transaction.id}`;
72+
} else if (errorDetail) {
73+
errorMessage = `${errorDetail.description} (${orderData.debug_id})`;
74+
} else {
75+
errorMessage = JSON.stringify(orderData);
76+
}
77+
78+
throw new Error(errorMessage);
79+
} else {
80+
// (3) Successful transaction -> Show confirmation or thank you message
81+
// Or go to another URL: actions.redirect('thank_you.html');
82+
resultMessage(
83+
`Transaction ${transaction.status}: ${transaction.id}<br><br>See console for all available details`,
84+
);
85+
console.log(
86+
"Capture result",
87+
orderData,
88+
JSON.stringify(orderData, null, 2),
89+
);
90+
}
91+
} catch (error) {
92+
console.error(error);
93+
resultMessage(
94+
`Sorry, your transaction could not be processed...<br><br>${error}`,
95+
);
96+
}
97+
}
98+
99+
window.paypal
100+
.Buttons({
101+
createOrder: createOrderCallback,
102+
onApprove: onApproveCallback,
103+
})
104+
.render("#paypal-button-container");
105+
106+
const cardField = window.paypal.CardFields({
107+
createOrder: createOrderCallback,
108+
onApprove: onApproveCallback,
109+
});
110+
111+
// Render each field after checking for eligibility
112+
if (cardField.isEligible()) {
113+
const nameField = cardField.NameField();
114+
nameField.render("#card-name-field-container");
115+
116+
const numberField = cardField.NumberField();
117+
numberField.render("#card-number-field-container");
118+
119+
const cvvField = cardField.CVVField();
120+
cvvField.render("#card-cvv-field-container");
121+
122+
const expiryField = cardField.ExpiryField();
123+
expiryField.render("#card-expiry-field-container");
124+
125+
// Add click listener to submit button and call the submit function on the CardField component
126+
document
127+
.getElementById("multi-card-field-button")
128+
.addEventListener("click", () => {
129+
cardField.submit();
130+
});
131+
} else {
132+
// Hides card fields if the merchant isn't eligible
133+
document.querySelector("#card-form").style = "display: none";
134+
}
135+
136+
// Example function to show a result to the user. Your site's UI library can be used instead.
137+
function resultMessage(message) {
138+
const container = document.querySelector("#result-message");
139+
container.innerHTML = message;
140+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "paypal-advanced-integration",
3+
"description": "Sample Node.js web app to integrate PayPal Advanced Checkout for online payments",
4+
"version": "1.0.0",
5+
"main": "server/server.js",
6+
"type": "module",
7+
"scripts": {
8+
"test": "echo \"Error: no test specified\" && exit 1",
9+
"start": "nodemon server/server.js",
10+
"format": "npx prettier --write **/*.{js,md}",
11+
"format:check": "npx prettier --check **/*.{js,md}",
12+
"lint": "npx eslint server/*.js --env=node && npx eslint client/*.js --env=browser"
13+
},
14+
"license": "Apache-2.0",
15+
"dependencies": {
16+
"dotenv": "^16.3.1",
17+
"express": "^4.18.2",
18+
"node-fetch": "^3.3.2"
19+
},
20+
"devDependencies": {
21+
"nodemon": "^3.0.1"
22+
}
23+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import "dotenv/config";
2+
import express from "express";
3+
import * as paypal from "./paypal-api.js";
4+
const { PORT = 8888 } = process.env;
5+
6+
const app = express();
7+
app.set("view engine", "ejs");
8+
app.use(express.static("public"));
9+
10+
// render checkout page with client id & unique client token
11+
app.get("/", async (req, res) => {
12+
const clientId = process.env.PAYPAL_CLIENT_ID;
13+
try {
14+
const clientToken = await paypal.generateClientToken();
15+
res.render("checkout", { clientId, clientToken });
16+
} catch (err) {
17+
res.status(500).send(err.message);
18+
}
19+
});
20+
21+
// create order
22+
app.post("/api/orders", async (req, res) => {
23+
try {
24+
const order = await paypal.createOrder();
25+
res.json(order);
26+
} catch (err) {
27+
res.status(500).send(err.message);
28+
}
29+
});
30+
31+
// capture payment
32+
app.post("/api/orders/:orderID/capture", async (req, res) => {
33+
const { orderID } = req.params;
34+
try {
35+
const captureData = await paypal.capturePayment(orderID);
36+
res.json(captureData);
37+
} catch (err) {
38+
res.status(500).send(err.message);
39+
}
40+
});
41+
42+
app.listen(PORT, () => {
43+
console.log(`Server listening at http://localhost:${PORT}/`);
44+
});
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import express from "express";
2+
import fetch from "node-fetch";
3+
import "dotenv/config";
4+
import path from "path";
5+
6+
const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PORT = 8888 } = process.env;
7+
const base = "https://api-m.sandbox.paypal.com";
8+
const app = express();
9+
10+
// host static files
11+
app.use(express.static("client"));
12+
13+
// parse post params sent in body in json format
14+
app.use(express.json());
15+
16+
/**
17+
* Generate an OAuth 2.0 access token for authenticating with PayPal REST APIs.
18+
* @see https://developer.paypal.com/api/rest/authentication/
19+
*/
20+
const generateAccessToken = async () => {
21+
try {
22+
if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) {
23+
throw new Error("MISSING_API_CREDENTIALS");
24+
}
25+
const auth = Buffer.from(
26+
PAYPAL_CLIENT_ID + ":" + PAYPAL_CLIENT_SECRET,
27+
).toString("base64");
28+
const response = await fetch(`${base}/v1/oauth2/token`, {
29+
method: "POST",
30+
body: "grant_type=client_credentials",
31+
headers: {
32+
Authorization: `Basic ${auth}`,
33+
},
34+
});
35+
36+
const data = await response.json();
37+
return data.access_token;
38+
} catch (error) {
39+
console.error("Failed to generate Access Token:", error);
40+
}
41+
};
42+
43+
/**
44+
* Create an order to start the transaction.
45+
* @see https://developer.paypal.com/docs/api/orders/v2/#orders_create
46+
*/
47+
const createOrder = async (cart) => {
48+
// use the cart information passed from the front-end to calculate the purchase unit details
49+
console.log(
50+
"shopping cart information passed from the frontend createOrder() callback:",
51+
cart,
52+
);
53+
54+
const accessToken = await generateAccessToken();
55+
const url = `${base}/v2/checkout/orders`;
56+
const payload = {
57+
intent: "CAPTURE",
58+
purchase_units: [
59+
{
60+
amount: {
61+
currency_code: "USD",
62+
value: "100.00",
63+
},
64+
},
65+
],
66+
};
67+
68+
const response = await fetch(url, {
69+
headers: {
70+
"Content-Type": "application/json",
71+
Authorization: `Bearer ${accessToken}`,
72+
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
73+
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
74+
// "PayPal-Mock-Response": '{"mock_application_codes": "MISSING_REQUIRED_PARAMETER"}'
75+
// "PayPal-Mock-Response": '{"mock_application_codes": "PERMISSION_DENIED"}'
76+
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
77+
},
78+
method: "POST",
79+
body: JSON.stringify(payload),
80+
});
81+
82+
return handleResponse(response);
83+
};
84+
85+
/**
86+
* Capture payment for the created order to complete the transaction.
87+
* @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture
88+
*/
89+
const captureOrder = async (orderID) => {
90+
const accessToken = await generateAccessToken();
91+
const url = `${base}/v2/checkout/orders/${orderID}/capture`;
92+
93+
const response = await fetch(url, {
94+
method: "POST",
95+
headers: {
96+
"Content-Type": "application/json",
97+
Authorization: `Bearer ${accessToken}`,
98+
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
99+
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
100+
// "PayPal-Mock-Response": '{"mock_application_codes": "INSTRUMENT_DECLINED"}'
101+
// "PayPal-Mock-Response": '{"mock_application_codes": "TRANSACTION_REFUSED"}'
102+
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
103+
},
104+
});
105+
106+
return handleResponse(response);
107+
};
108+
109+
async function handleResponse(response) {
110+
try {
111+
const jsonResponse = await response.json();
112+
return {
113+
jsonResponse,
114+
httpStatusCode: response.status,
115+
};
116+
} catch (err) {
117+
const errorMessage = await response.text();
118+
throw new Error(errorMessage);
119+
}
120+
}
121+
122+
app.post("/api/orders", async (req, res) => {
123+
try {
124+
// use the cart information passed from the front-end to calculate the order amount detals
125+
const { cart } = req.body;
126+
const { jsonResponse, httpStatusCode } = await createOrder(cart);
127+
res.status(httpStatusCode).json(jsonResponse);
128+
} catch (error) {
129+
console.error("Failed to create order:", error);
130+
res.status(500).json({ error: "Failed to create order." });
131+
}
132+
});
133+
134+
app.post("/api/orders/:orderID/capture", async (req, res) => {
135+
try {
136+
const { orderID } = req.params;
137+
const { jsonResponse, httpStatusCode } = await captureOrder(orderID);
138+
res.status(httpStatusCode).json(jsonResponse);
139+
} catch (error) {
140+
console.error("Failed to create order:", error);
141+
res.status(500).json({ error: "Failed to capture order." });
142+
}
143+
});
144+
145+
// serve index.html
146+
app.get("/", (req, res) => {
147+
res.sendFile(path.resolve("./client/checkout.html"));
148+
});
149+
150+
app.listen(PORT, () => {
151+
console.log(`Node server listening at http://localhost:${PORT}/`);
152+
});

0 commit comments

Comments
 (0)