diff --git a/save-payment-method/.env.example b/save-payment-method/.env.example
new file mode 100644
index 00000000..2251fbbb
--- /dev/null
+++ b/save-payment-method/.env.example
@@ -0,0 +1,5 @@
+# Create an application to obtain credentials at
+# https://developer.paypal.com/dashboard/applications/sandbox
+
+PAYPAL_CLIENT_ID=YOUR_CLIENT_ID_GOES_HERE
+PAYPAL_CLIENT_SECRET=YOUR_SECRET_GOES_HERE
diff --git a/save-payment-method/.gitignore b/save-payment-method/.gitignore
new file mode 100644
index 00000000..4c49bd78
--- /dev/null
+++ b/save-payment-method/.gitignore
@@ -0,0 +1 @@
+.env
diff --git a/save-payment-method/README.md b/save-payment-method/README.md
new file mode 100644
index 00000000..f1f4a0e9
--- /dev/null
+++ b/save-payment-method/README.md
@@ -0,0 +1,15 @@
+# Save Payment Method Example
+
+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.
+
+[View the Documentation](https://developer.paypal.com/docs/checkout/save-payment-methods/during-purchase/js-sdk/paypal/)
+
+## Instructions
+
+1. [Create an application](https://developer.paypal.com/dashboard/applications/sandbox/create)
+2. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET`
+3. Replace `test` in [client/app.js](client/app.js) with your app's client-id
+4. Run `npm install`
+5. Run `npm start`
+6. Open http://localhost:8888
+7. Click "PayPal" and log in with one of your [Sandbox test accounts](https://developer.paypal.com/dashboard/accounts)
diff --git a/save-payment-method/client/app.js b/save-payment-method/client/app.js
new file mode 100644
index 00000000..eebd2017
--- /dev/null
+++ b/save-payment-method/client/app.js
@@ -0,0 +1,97 @@
+window.paypal
+ .Buttons({
+ async createOrder() {
+ try {
+ const response = await fetch("/api/orders", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ // use the "body" param to optionally pass additional order information
+ // like product ids and quantities
+ body: JSON.stringify({
+ cart: [
+ {
+ id: "YOUR_PRODUCT_ID",
+ quantity: "YOUR_PRODUCT_QUANTITY",
+ },
+ ],
+ }),
+ });
+
+ const orderData = await response.json();
+
+ if (orderData.id) {
+ return orderData.id;
+ } else {
+ const errorDetail = orderData?.details?.[0];
+ const errorMessage = errorDetail
+ ? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
+ : JSON.stringify(orderData);
+
+ throw new Error(errorMessage);
+ }
+ } catch (error) {
+ console.error(error);
+ resultMessage(`Could not initiate PayPal Checkout...
${error}`);
+ }
+ },
+ async onApprove(data, actions) {
+ try {
+ const response = await fetch(`/api/orders/${data.orderID}/capture`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const orderData = await response.json();
+ // Three cases to handle:
+ // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
+ // (2) Other non-recoverable errors -> Show a failure message
+ // (3) Successful transaction -> Show confirmation or thank you message
+
+ const errorDetail = orderData?.details?.[0];
+
+ if (errorDetail?.issue === "INSTRUMENT_DECLINED") {
+ // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
+ // recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
+ return actions.restart();
+ } else if (errorDetail) {
+ // (2) Other non-recoverable errors -> Show a failure message
+ throw new Error(`${errorDetail.description} (${orderData.debug_id})`);
+ } else if (!orderData.purchase_units) {
+ throw new Error(JSON.stringify(orderData));
+ } else {
+ // (3) Successful transaction -> Show confirmation or thank you message
+ // Or go to another URL: actions.redirect('thank_you.html');
+ const transaction =
+ orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
+ orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
+ resultMessage(
+ `Transaction ${transaction.status}: ${transaction.id}
See console for all available details.
+ See the return buyer experience
+ `,
+ );
+
+ console.log(
+ "Capture result",
+ orderData,
+ JSON.stringify(orderData, null, 2),
+ );
+ }
+ } catch (error) {
+ console.error(error);
+ resultMessage(
+ `Sorry, your transaction could not be processed...
${error}`,
+ );
+ }
+ },
+ })
+ .render("#paypal-button-container");
+
+// Example function to show a result to the user. Your site's UI library can be used instead.
+function resultMessage(message) {
+ const container = document.querySelector("#result-message");
+ container.innerHTML = message;
+}
diff --git a/save-payment-method/package.json b/save-payment-method/package.json
new file mode 100644
index 00000000..858c68af
--- /dev/null
+++ b/save-payment-method/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "paypal-save-payment-method",
+ "description": "Sample Node.js web app to integrate PayPal Save Payment Method for online payments",
+ "version": "1.0.0",
+ "main": "server/server.js",
+ "type": "module",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "start": "nodemon server/server.js",
+ "format": "npx prettier --write **/*.{js,md}",
+ "format:check": "npx prettier --check **/*.{js,md}",
+ "lint": "npx eslint server/*.js --env=node && npx eslint client/*.js --env=browser"
+ },
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dotenv": "^16.3.1",
+ "ejs": "^3.1.9",
+ "express": "^4.18.2",
+ "node-fetch": "^3.3.2"
+ },
+ "devDependencies": {
+ "nodemon": "^3.0.1"
+ }
+}
diff --git a/save-payment-method/server/server.js b/save-payment-method/server/server.js
new file mode 100644
index 00000000..7d2596f2
--- /dev/null
+++ b/save-payment-method/server/server.js
@@ -0,0 +1,193 @@
+import express from "express";
+import fetch from "node-fetch";
+import "dotenv/config";
+
+const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PORT = 8888 } = process.env;
+const base = "https://api-m.sandbox.paypal.com";
+const app = express();
+
+app.set("view engine", "ejs");
+app.set("views", "./server/views");
+
+// host static files
+app.use(express.static("client"));
+
+// parse post params sent in body in json format
+app.use(express.json());
+
+/**
+ * Generate an OAuth 2.0 access token for authenticating with PayPal REST APIs.
+ * @see https://developer.paypal.com/api/rest/authentication/
+ */
+const authenticate = async (bodyParams) => {
+ const params = {
+ grant_type: "client_credentials",
+ response_type: "id_token",
+ ...bodyParams,
+ };
+
+ // pass the url encoded value as the body of the post call
+ const urlEncodedParams = new URLSearchParams(params).toString();
+ try {
+ if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) {
+ throw new Error("MISSING_API_CREDENTIALS");
+ }
+ const auth = Buffer.from(
+ PAYPAL_CLIENT_ID + ":" + PAYPAL_CLIENT_SECRET,
+ ).toString("base64");
+
+ const response = await fetch(`${base}/v1/oauth2/token`, {
+ method: "POST",
+ body: urlEncodedParams,
+ headers: {
+ Authorization: `Basic ${auth}`,
+ },
+ });
+ return handleResponse(response);
+ } catch (error) {
+ console.error("Failed to generate Access Token:", error);
+ }
+};
+
+const generateAccessToken = async () => {
+ const { jsonResponse } = await authenticate();
+ return jsonResponse.access_token;
+};
+
+/**
+ * Create an order to start the transaction.
+ * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create
+ */
+const createOrder = async (cart) => {
+ // use the cart information passed from the front-end to calculate the purchase unit details
+ console.log(
+ "shopping cart information passed from the frontend createOrder() callback:",
+ cart,
+ );
+
+ const accessToken = await generateAccessToken();
+ const url = `${base}/v2/checkout/orders`;
+ const payload = {
+ intent: "CAPTURE",
+ purchase_units: [
+ {
+ amount: {
+ currency_code: "USD",
+ value: "110.00",
+ },
+ },
+ ],
+ payment_source: {
+ paypal: {
+ attributes: {
+ vault: {
+ store_in_vault: "ON_SUCCESS",
+ usage_type: "MERCHANT",
+ customer_type: "CONSUMER",
+ },
+ },
+ experience_context: {
+ return_url: "http://example.com",
+ cancel_url: "http://example.com",
+ shipping_preference: "NO_SHIPPING",
+ },
+ },
+ },
+ };
+
+ const response = await fetch(url, {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${accessToken}`,
+ // Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
+ // https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
+ // "PayPal-Mock-Response": '{"mock_application_codes": "MISSING_REQUIRED_PARAMETER"}'
+ // "PayPal-Mock-Response": '{"mock_application_codes": "PERMISSION_DENIED"}'
+ // "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
+ },
+ method: "POST",
+ body: JSON.stringify(payload),
+ });
+
+ return handleResponse(response);
+};
+
+/**
+ * Capture payment for the created order to complete the transaction.
+ * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture
+ */
+const captureOrder = async (orderID) => {
+ const accessToken = await generateAccessToken();
+ const url = `${base}/v2/checkout/orders/${orderID}/capture`;
+
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${accessToken}`,
+ // Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
+ // https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
+ // "PayPal-Mock-Response": '{"mock_application_codes": "INSTRUMENT_DECLINED"}'
+ // "PayPal-Mock-Response": '{"mock_application_codes": "TRANSACTION_REFUSED"}'
+ // "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
+ },
+ });
+
+ return handleResponse(response);
+};
+
+async function handleResponse(response) {
+ try {
+ const jsonResponse = await response.json();
+ return {
+ jsonResponse,
+ httpStatusCode: response.status,
+ };
+ } catch (err) {
+ const errorMessage = await response.text();
+ throw new Error(errorMessage);
+ }
+}
+
+app.post("/api/orders", async (req, res) => {
+ try {
+ // use the cart information passed from the front-end to calculate the order amount detals
+ const { cart } = req.body;
+ const { jsonResponse, httpStatusCode } = await createOrder(cart);
+ res.status(httpStatusCode).json(jsonResponse);
+ } catch (error) {
+ console.error("Failed to create order:", error);
+ res.status(500).json({ error: "Failed to create order." });
+ }
+});
+
+app.post("/api/orders/:orderID/capture", async (req, res) => {
+ try {
+ const { orderID } = req.params;
+ const { jsonResponse, httpStatusCode } = await captureOrder(orderID);
+ console.log("capture response", jsonResponse);
+ res.status(httpStatusCode).json(jsonResponse);
+ } catch (error) {
+ console.error("Failed to create order:", error);
+ res.status(500).json({ error: "Failed to capture order." });
+ }
+});
+
+// render checkout page with client id & user id token
+app.get("/", async (req, res) => {
+ try {
+ const { jsonResponse } = await authenticate({
+ target_customer_id: req.query.customerID,
+ });
+ res.render("checkout", {
+ clientId: PAYPAL_CLIENT_ID,
+ userIdToken: jsonResponse.id_token,
+ });
+ } catch (err) {
+ res.status(500).send(err.message);
+ }
+});
+
+app.listen(PORT, () => {
+ console.log(`Node server listening at http://localhost:${PORT}/`);
+});
diff --git a/save-payment-method/server/views/checkout.ejs b/save-payment-method/server/views/checkout.ejs
new file mode 100644
index 00000000..fa995630
--- /dev/null
+++ b/save-payment-method/server/views/checkout.ejs
@@ -0,0 +1,17 @@
+
+
+