diff --git a/advanced-integration/README.md b/advanced-integration/README.md
index 923a5234..96d2c205 100644
--- a/advanced-integration/README.md
+++ b/advanced-integration/README.md
@@ -1,9 +1,14 @@
-# Advanced Integration Example
+# Advanced Checkout Integration Example
-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.
+This folder contains example code for a PayPal advanced Checkout integration using both the JavaScript SDK and Node.js to complete transactions with the PayPal REST API.
+
+* [`v2`](v2/README.md) contains sample code for the current advanced Checkout integration. This includes guidance on using Hosted Card Fields.
+* [`v1`](v1/README.md) contains sample code for the legacy advanced Checkout integration. Use `v2` for new integrations.
## Instructions
+These instructions apply to the sample code for both `v2` and `v1`:
+
1. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET`.
2. Run `npm install`
3. Run `npm start`
diff --git a/advanced-integration/beta/README.md b/advanced-integration/beta/README.md
deleted file mode 100644
index 2f6e5140..00000000
--- a/advanced-integration/beta/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Advanced Integration Example
-
-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.
-
-## 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/checkout.html](client/checkout.html) with your app's client-id
-4. Run `npm install`
-5. Run `npm start`
-6. Open http://localhost:8888
-7. 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)
diff --git a/advanced-integration/package.json b/advanced-integration/package.json
deleted file mode 100644
index d17aa000..00000000
--- a/advanced-integration/package.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "name": "paypal-advanced-integration",
- "description": "Sample Node.js web app to integrate PayPal Advanced Checkout 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.js",
- "format": "npx prettier --write **/*.{js,md}",
- "format:check": "npx prettier --check **/*.{js,md}",
- "lint": "npx eslint server.js paypal-api.js --env=node && npx eslint public/*.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/advanced-integration/v1/.gitignore b/advanced-integration/v1/.gitignore
new file mode 100644
index 00000000..4c49bd78
--- /dev/null
+++ b/advanced-integration/v1/.gitignore
@@ -0,0 +1 @@
+.env
diff --git a/advanced-integration/v1/README.md b/advanced-integration/v1/README.md
index 923a5234..152ef9ae 100644
--- a/advanced-integration/v1/README.md
+++ b/advanced-integration/v1/README.md
@@ -1,11 +1,14 @@
# Advanced Integration Example
-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.
+This folder contains example code for [version 1](https://developer.paypal.com/docs/checkout/advanced/integrate/sdk/v1) of a PayPal advanced Checkout integration using the JavaScript SDK and Node.js to complete transactions with the PayPal REST API.
+
+> **Note:** Version 1 is a legacy integration. Use [version 2](https://developer.paypal.com/docs/checkout/advanced/integrate/) for new integrations.
## Instructions
-1. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET`.
-2. Run `npm install`
-3. Run `npm start`
-4. Open http://localhost:8888
-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)
+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. Run `npm install`.
+4. Run `npm start`.
+5. Open http://localhost:8888.
+6. 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).
diff --git a/advanced-integration/v1/client/app.js b/advanced-integration/v1/client/app.js
deleted file mode 100644
index 65f048d7..00000000
--- a/advanced-integration/v1/client/app.js
+++ /dev/null
@@ -1,186 +0,0 @@
-async function createOrderCallback() {
- 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 function onApproveCallback(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 transaction =
- orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
- orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
- const errorDetail = orderData?.details?.[0];
-
- // this actions.restart() behavior only applies to the Buttons component
- if (errorDetail?.issue === "INSTRUMENT_DECLINED" && !data.card && actions) {
- // (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 ||
- !transaction ||
- transaction.status === "DECLINED"
- ) {
- // (2) Other non-recoverable errors -> Show a failure message
- let errorMessage;
- if (transaction) {
- errorMessage = `Transaction ${transaction.status}: ${transaction.id}`;
- } else if (errorDetail) {
- errorMessage = `${errorDetail.description} (${orderData.debug_id})`;
- } else {
- errorMessage = JSON.stringify(orderData);
- }
-
- throw new Error(errorMessage);
- } else {
- // (3) Successful transaction -> Show confirmation or thank you message
- // Or go to another URL: actions.redirect('thank_you.html');
- resultMessage(
- `Transaction ${transaction.status}: ${transaction.id}
See console for all available details`,
- );
- console.log(
- "Capture result",
- orderData,
- JSON.stringify(orderData, null, 2),
- );
- }
- } catch (error) {
- console.error(error);
- resultMessage(
- `Sorry, your transaction could not be processed...
${error}`,
- );
- }
-}
-
-window.paypal
- .Buttons({
- createOrder: createOrderCallback,
- onApprove: onApproveCallback,
- })
- .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;
-}
-
-// If this returns false or the card fields aren't visible, see Step #1.
-if (window.paypal.HostedFields.isEligible()) {
- // Renders card fields
- window.paypal.HostedFields.render({
- // Call your server to set up the transaction
- createOrder: createOrderCallback,
- styles: {
- ".valid": {
- color: "green",
- },
- ".invalid": {
- color: "red",
- },
- },
- fields: {
- number: {
- selector: "#card-number",
- placeholder: "4111 1111 1111 1111",
- },
- cvv: {
- selector: "#cvv",
- placeholder: "123",
- },
- expirationDate: {
- selector: "#expiration-date",
- placeholder: "MM/YY",
- },
- },
- }).then((cardFields) => {
- document.querySelector("#card-form").addEventListener("submit", (event) => {
- event.preventDefault();
- cardFields
- .submit({
- // Cardholder's first and last name
- cardholderName: document.getElementById("card-holder-name").value,
- // Billing Address
- billingAddress: {
- // Street address, line 1
- streetAddress: document.getElementById(
- "card-billing-address-street",
- ).value,
- // Street address, line 2 (Ex: Unit, Apartment, etc.)
- extendedAddress: document.getElementById(
- "card-billing-address-unit",
- ).value,
- // State
- region: document.getElementById("card-billing-address-state").value,
- // City
- locality: document.getElementById("card-billing-address-city")
- .value,
- // Postal Code
- postalCode: document.getElementById("card-billing-address-zip")
- .value,
- // Country Code
- countryCodeAlpha2: document.getElementById(
- "card-billing-address-country",
- ).value,
- },
- })
- .then((data) => {
- return onApproveCallback(data);
- })
- .catch((orderData) => {
- resultMessage(
- `Sorry, your transaction could not be processed...
${JSON.stringify(
- orderData,
- )}`,
- );
- });
- });
- });
-} else {
- // Hides card fields if the merchant isn't eligible
- document.querySelector("#card-form").style = "display: none";
-}
diff --git a/advanced-integration/.env.example b/advanced-integration/v1/env.example
similarity index 100%
rename from advanced-integration/.env.example
rename to advanced-integration/v1/env.example
diff --git a/advanced-integration/v1/package.json b/advanced-integration/v1/package.json
index ff3f5b41..d17aa000 100644
--- a/advanced-integration/v1/package.json
+++ b/advanced-integration/v1/package.json
@@ -6,10 +6,10 @@
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
- "start": "nodemon server/server.js",
+ "start": "nodemon 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"
+ "lint": "npx eslint server.js paypal-api.js --env=node && npx eslint public/*.js --env=browser"
},
"license": "Apache-2.0",
"dependencies": {
diff --git a/advanced-integration/paypal-api.js b/advanced-integration/v1/paypal-api.js
similarity index 100%
rename from advanced-integration/paypal-api.js
rename to advanced-integration/v1/paypal-api.js
diff --git a/advanced-integration/public/app.js b/advanced-integration/v1/public/app.js
similarity index 100%
rename from advanced-integration/public/app.js
rename to advanced-integration/v1/public/app.js
diff --git a/advanced-integration/server.js b/advanced-integration/v1/server.js
similarity index 100%
rename from advanced-integration/server.js
rename to advanced-integration/v1/server.js
diff --git a/advanced-integration/v1/server/server.js b/advanced-integration/v1/server/server.js
deleted file mode 100644
index a7d84407..00000000
--- a/advanced-integration/v1/server/server.js
+++ /dev/null
@@ -1,178 +0,0 @@
-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");
-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 generateAccessToken = async () => {
- 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: "grant_type=client_credentials",
- headers: {
- Authorization: `Basic ${auth}`,
- },
- });
-
- const data = await response.json();
- return data.access_token;
- } catch (error) {
- console.error("Failed to generate Access Token:", error);
- }
-};
-
-/**
- * Generate a client token for rendering the hosted card fields.
- * @see https://developer.paypal.com/docs/checkout/advanced/integrate/#link-integratebackend
- */
-const generateClientToken = async () => {
- const accessToken = await generateAccessToken();
- const url = `${base}/v1/identity/generate-token`;
- const response = await fetch(url, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${accessToken}`,
- "Accept-Language": "en_US",
- "Content-Type": "application/json",
- },
- });
-
- return handleResponse(response);
-};
-
-/**
- * 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: "100.00",
- },
- },
- ],
- };
-
- 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);
- }
-}
-
-// render checkout page with client id & unique client token
-app.get("/", async (req, res) => {
- try {
- const { jsonResponse } = await generateClientToken();
- res.render("checkout", {
- clientId: PAYPAL_CLIENT_ID,
- clientToken: jsonResponse.client_token,
- });
- } catch (err) {
- res.status(500).send(err.message);
- }
-});
-
-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);
- res.status(httpStatusCode).json(jsonResponse);
- } catch (error) {
- console.error("Failed to create order:", error);
- res.status(500).json({ error: "Failed to capture order." });
- }
-});
-
-app.listen(PORT, () => {
- console.log(`Node server listening at http://localhost:${PORT}/`);
-});
diff --git a/advanced-integration/v1/server/views/checkout.ejs b/advanced-integration/v1/views/checkout.ejs
similarity index 88%
rename from advanced-integration/v1/server/views/checkout.ejs
rename to advanced-integration/v1/views/checkout.ejs
index 85cd7085..12522326 100644
--- a/advanced-integration/v1/server/views/checkout.ejs
+++ b/advanced-integration/v1/views/checkout.ejs
@@ -1,14 +1,12 @@
-
-
+