Skip to content

Commit 4f82e50

Browse files
authored
feat: add example for saving a payment method (#99)
1 parent 4603a51 commit 4f82e50

File tree

7 files changed

+352
-0
lines changed

7 files changed

+352
-0
lines changed

save-payment-method/.env.example

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Create an application to obtain credentials at
2+
# https://developer.paypal.com/dashboard/applications/sandbox
3+
4+
PAYPAL_CLIENT_ID=YOUR_CLIENT_ID_GOES_HERE
5+
PAYPAL_CLIENT_SECRET=YOUR_SECRET_GOES_HERE

save-payment-method/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.env

save-payment-method/README.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Save Payment Method Example
2+
3+
This folder contains example code for a PayPal Save Payment Method integration using both the JS SDK and Node.js to complete transactions with the PayPal REST API.
4+
5+
[View the Documentation](https://developer.paypal.com/docs/checkout/save-payment-methods/during-purchase/js-sdk/paypal/)
6+
7+
## Instructions
8+
9+
1. [Create an application](https://developer.paypal.com/dashboard/applications/sandbox/create)
10+
2. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET`
11+
3. Replace `test` in [client/app.js](client/app.js) with your app's client-id
12+
4. Run `npm install`
13+
5. Run `npm start`
14+
6. Open http://localhost:8888
15+
7. Click "PayPal" and log in with one of your [Sandbox test accounts](https://developer.paypal.com/dashboard/accounts)

save-payment-method/client/app.js

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
window.paypal
2+
.Buttons({
3+
async createOrder() {
4+
try {
5+
const response = await fetch("/api/orders", {
6+
method: "POST",
7+
headers: {
8+
"Content-Type": "application/json",
9+
},
10+
// use the "body" param to optionally pass additional order information
11+
// like product ids and quantities
12+
body: JSON.stringify({
13+
cart: [
14+
{
15+
id: "YOUR_PRODUCT_ID",
16+
quantity: "YOUR_PRODUCT_QUANTITY",
17+
},
18+
],
19+
}),
20+
});
21+
22+
const orderData = await response.json();
23+
24+
if (orderData.id) {
25+
return orderData.id;
26+
} else {
27+
const errorDetail = orderData?.details?.[0];
28+
const errorMessage = errorDetail
29+
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
30+
: JSON.stringify(orderData);
31+
32+
throw new Error(errorMessage);
33+
}
34+
} catch (error) {
35+
console.error(error);
36+
resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
37+
}
38+
},
39+
async onApprove(data, actions) {
40+
try {
41+
const response = await fetch(`/api/orders/${data.orderID}/capture`, {
42+
method: "POST",
43+
headers: {
44+
"Content-Type": "application/json",
45+
},
46+
});
47+
48+
const orderData = await response.json();
49+
// Three cases to handle:
50+
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
51+
// (2) Other non-recoverable errors -> Show a failure message
52+
// (3) Successful transaction -> Show confirmation or thank you message
53+
54+
const errorDetail = orderData?.details?.[0];
55+
56+
if (errorDetail?.issue === "INSTRUMENT_DECLINED") {
57+
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
58+
// recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
59+
return actions.restart();
60+
} else if (errorDetail) {
61+
// (2) Other non-recoverable errors -> Show a failure message
62+
throw new Error(`${errorDetail.description} (${orderData.debug_id})`);
63+
} else if (!orderData.purchase_units) {
64+
throw new Error(JSON.stringify(orderData));
65+
} else {
66+
// (3) Successful transaction -> Show confirmation or thank you message
67+
// Or go to another URL: actions.redirect('thank_you.html');
68+
const transaction =
69+
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
70+
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
71+
resultMessage(
72+
`Transaction ${transaction.status}: ${transaction.id}<br><br>See console for all available details.<br>
73+
<a href='/?customerID=${orderData.payment_source.paypal.attributes.vault.customer.id}'>See the return buyer experience</a>
74+
`,
75+
);
76+
77+
console.log(
78+
"Capture result",
79+
orderData,
80+
JSON.stringify(orderData, null, 2),
81+
);
82+
}
83+
} catch (error) {
84+
console.error(error);
85+
resultMessage(
86+
`Sorry, your transaction could not be processed...<br><br>${error}`,
87+
);
88+
}
89+
},
90+
})
91+
.render("#paypal-button-container");
92+
93+
// Example function to show a result to the user. Your site's UI library can be used instead.
94+
function resultMessage(message) {
95+
const container = document.querySelector("#result-message");
96+
container.innerHTML = message;
97+
}

save-payment-method/package.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "paypal-save-payment-method",
3+
"description": "Sample Node.js web app to integrate PayPal Save Payment Method 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+
"ejs": "^3.1.9",
18+
"express": "^4.18.2",
19+
"node-fetch": "^3.3.2"
20+
},
21+
"devDependencies": {
22+
"nodemon": "^3.0.1"
23+
}
24+
}

save-payment-method/server/server.js

+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import express from "express";
2+
import fetch from "node-fetch";
3+
import "dotenv/config";
4+
5+
const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PORT = 8888 } = process.env;
6+
const base = "https://api-m.sandbox.paypal.com";
7+
const app = express();
8+
9+
app.set("view engine", "ejs");
10+
app.set("views", "./server/views");
11+
12+
// host static files
13+
app.use(express.static("client"));
14+
15+
// parse post params sent in body in json format
16+
app.use(express.json());
17+
18+
/**
19+
* Generate an OAuth 2.0 access token for authenticating with PayPal REST APIs.
20+
* @see https://developer.paypal.com/api/rest/authentication/
21+
*/
22+
const authenticate = async (bodyParams) => {
23+
const params = {
24+
grant_type: "client_credentials",
25+
response_type: "id_token",
26+
...bodyParams,
27+
};
28+
29+
// pass the url encoded value as the body of the post call
30+
const urlEncodedParams = new URLSearchParams(params).toString();
31+
try {
32+
if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) {
33+
throw new Error("MISSING_API_CREDENTIALS");
34+
}
35+
const auth = Buffer.from(
36+
PAYPAL_CLIENT_ID + ":" + PAYPAL_CLIENT_SECRET,
37+
).toString("base64");
38+
39+
const response = await fetch(`${base}/v1/oauth2/token`, {
40+
method: "POST",
41+
body: urlEncodedParams,
42+
headers: {
43+
Authorization: `Basic ${auth}`,
44+
},
45+
});
46+
return handleResponse(response);
47+
} catch (error) {
48+
console.error("Failed to generate Access Token:", error);
49+
}
50+
};
51+
52+
const generateAccessToken = async () => {
53+
const { jsonResponse } = await authenticate();
54+
return jsonResponse.access_token;
55+
};
56+
57+
/**
58+
* Create an order to start the transaction.
59+
* @see https://developer.paypal.com/docs/api/orders/v2/#orders_create
60+
*/
61+
const createOrder = async (cart) => {
62+
// use the cart information passed from the front-end to calculate the purchase unit details
63+
console.log(
64+
"shopping cart information passed from the frontend createOrder() callback:",
65+
cart,
66+
);
67+
68+
const accessToken = await generateAccessToken();
69+
const url = `${base}/v2/checkout/orders`;
70+
const payload = {
71+
intent: "CAPTURE",
72+
purchase_units: [
73+
{
74+
amount: {
75+
currency_code: "USD",
76+
value: "110.00",
77+
},
78+
},
79+
],
80+
payment_source: {
81+
paypal: {
82+
attributes: {
83+
vault: {
84+
store_in_vault: "ON_SUCCESS",
85+
usage_type: "MERCHANT",
86+
customer_type: "CONSUMER",
87+
},
88+
},
89+
experience_context: {
90+
return_url: "http://example.com",
91+
cancel_url: "http://example.com",
92+
shipping_preference: "NO_SHIPPING",
93+
},
94+
},
95+
},
96+
};
97+
98+
const response = await fetch(url, {
99+
headers: {
100+
"Content-Type": "application/json",
101+
Authorization: `Bearer ${accessToken}`,
102+
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
103+
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
104+
// "PayPal-Mock-Response": '{"mock_application_codes": "MISSING_REQUIRED_PARAMETER"}'
105+
// "PayPal-Mock-Response": '{"mock_application_codes": "PERMISSION_DENIED"}'
106+
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
107+
},
108+
method: "POST",
109+
body: JSON.stringify(payload),
110+
});
111+
112+
return handleResponse(response);
113+
};
114+
115+
/**
116+
* Capture payment for the created order to complete the transaction.
117+
* @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture
118+
*/
119+
const captureOrder = async (orderID) => {
120+
const accessToken = await generateAccessToken();
121+
const url = `${base}/v2/checkout/orders/${orderID}/capture`;
122+
123+
const response = await fetch(url, {
124+
method: "POST",
125+
headers: {
126+
"Content-Type": "application/json",
127+
Authorization: `Bearer ${accessToken}`,
128+
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
129+
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
130+
// "PayPal-Mock-Response": '{"mock_application_codes": "INSTRUMENT_DECLINED"}'
131+
// "PayPal-Mock-Response": '{"mock_application_codes": "TRANSACTION_REFUSED"}'
132+
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
133+
},
134+
});
135+
136+
return handleResponse(response);
137+
};
138+
139+
async function handleResponse(response) {
140+
try {
141+
const jsonResponse = await response.json();
142+
return {
143+
jsonResponse,
144+
httpStatusCode: response.status,
145+
};
146+
} catch (err) {
147+
const errorMessage = await response.text();
148+
throw new Error(errorMessage);
149+
}
150+
}
151+
152+
app.post("/api/orders", async (req, res) => {
153+
try {
154+
// use the cart information passed from the front-end to calculate the order amount detals
155+
const { cart } = req.body;
156+
const { jsonResponse, httpStatusCode } = await createOrder(cart);
157+
res.status(httpStatusCode).json(jsonResponse);
158+
} catch (error) {
159+
console.error("Failed to create order:", error);
160+
res.status(500).json({ error: "Failed to create order." });
161+
}
162+
});
163+
164+
app.post("/api/orders/:orderID/capture", async (req, res) => {
165+
try {
166+
const { orderID } = req.params;
167+
const { jsonResponse, httpStatusCode } = await captureOrder(orderID);
168+
console.log("capture response", jsonResponse);
169+
res.status(httpStatusCode).json(jsonResponse);
170+
} catch (error) {
171+
console.error("Failed to create order:", error);
172+
res.status(500).json({ error: "Failed to capture order." });
173+
}
174+
});
175+
176+
// render checkout page with client id & user id token
177+
app.get("/", async (req, res) => {
178+
try {
179+
const { jsonResponse } = await authenticate({
180+
target_customer_id: req.query.customerID,
181+
});
182+
res.render("checkout", {
183+
clientId: PAYPAL_CLIENT_ID,
184+
userIdToken: jsonResponse.id_token,
185+
});
186+
} catch (err) {
187+
res.status(500).send(err.message);
188+
}
189+
});
190+
191+
app.listen(PORT, () => {
192+
console.log(`Node server listening at http://localhost:${PORT}/`);
193+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
<title>PayPal JS SDK Save Payment Method Integration</title>
7+
</head>
8+
<body>
9+
<div id="paypal-button-container"></div>
10+
<p id="result-message"></p>
11+
<script
12+
src="https://www.paypal.com/sdk/js?client-id=<%= clientId %>&vault=true"
13+
data-user-id-token="<%= userIdToken %>"
14+
></script>
15+
<script src="app.js"></script>
16+
</body>
17+
</html>

0 commit comments

Comments
 (0)