diff --git a/.devcontainer/advanced-integration-beta/devcontainer.json b/.devcontainer/advanced-integration-beta/devcontainer.json
new file mode 100644
index 00000000..44c2d10c
--- /dev/null
+++ b/.devcontainer/advanced-integration-beta/devcontainer.json
@@ -0,0 +1,40 @@
+// For more details, see https://aka.ms/devcontainer.json.
+{
+ "name": "PayPal Advanced Integration (beta)",
+ "image": "mcr.microsoft.com/devcontainers/javascript-node:20",
+ "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration/beta",
+ // Use 'onCreateCommand' to run commands when creating the container.
+ "onCreateCommand": "bash ../.devcontainer/advanced-integration-beta/welcome-message.sh",
+ // Use 'postCreateCommand' to run commands after the container is created.
+ "postCreateCommand": "npm install",
+ // Use 'postAttachCommand' to run commands when attaching to the container.
+ "postAttachCommand": {
+ "Start server": "npm start"
+ },
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
+ "forwardPorts": [8888],
+ "portsAttributes": {
+ "8888": {
+ "label": "Preview of Advanced Checkout Flow",
+ "onAutoForward": "openBrowserOnce"
+ }
+ },
+ "secrets": {
+ "PAYPAL_CLIENT_ID": {
+ "description": "Sandbox client ID of the application.",
+ "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox"
+ },
+ "PAYPAL_CLIENT_SECRET": {
+ "description": "Sandbox secret of the application.",
+ "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox"
+ }
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": ["vsls-contrib.codetour"],
+ "settings": {
+ "git.openRepositoryInParentFolders": "always"
+ }
+ }
+ }
+}
diff --git a/.devcontainer/advanced-integration-beta/welcome-message.sh b/.devcontainer/advanced-integration-beta/welcome-message.sh
new file mode 100644
index 00000000..ae9a72f9
--- /dev/null
+++ b/.devcontainer/advanced-integration-beta/welcome-message.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+set -e
+
+WELCOME_MESSAGE="
+👋 Welcome to the \"PayPal Advanced Checkout Integration Example\"
+
+🛠️ Your environment is fully setup with all the required software.
+
+🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted."
+
+ALTERNATE_WELCOME_MESSAGE="
+👋 Welcome to the \"PayPal Advanced Checkout Integration Example\"
+
+🛠️ Your environment is fully setup with all the required software.
+
+🚀 The checkout page will automatically open in the browser after the server is started."
+
+if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then
+ WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}"
+fi
+
+sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt"
diff --git a/advanced-integration/beta/.env.example b/advanced-integration/beta/.env.example
new file mode 100644
index 00000000..2251fbbb
--- /dev/null
+++ b/advanced-integration/beta/.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/advanced-integration/beta/README.md b/advanced-integration/beta/README.md
new file mode 100644
index 00000000..923a5234
--- /dev/null
+++ b/advanced-integration/beta/README.md
@@ -0,0 +1,11 @@
+# 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. 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)
diff --git a/advanced-integration/beta/client/checkout.html b/advanced-integration/beta/client/checkout.html
new file mode 100644
index 00000000..a052a57a
--- /dev/null
+++ b/advanced-integration/beta/client/checkout.html
@@ -0,0 +1,24 @@
+
+
+
${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");
+
+const cardField = window.paypal.CardFields({
+ createOrder: createOrderCallback,
+ onApprove: onApproveCallback,
+});
+
+// Render each field after checking for eligibility
+if (cardField.isEligible()) {
+ const nameField = cardField.NameField();
+ nameField.render("#card-name-field-container");
+
+ const numberField = cardField.NumberField();
+ numberField.render("#card-number-field-container");
+
+ const cvvField = cardField.CVVField();
+ cvvField.render("#card-cvv-field-container");
+
+ const expiryField = cardField.ExpiryField();
+ expiryField.render("#card-expiry-field-container");
+
+ // Add click listener to submit button and call the submit function on the CardField component
+ document
+ .getElementById("multi-card-field-button")
+ .addEventListener("click", () => {
+ cardField.submit().catch((error) => {
+ resultMessage(
+ `Sorry, your transaction could not be processed...
${error}`,
+ );
+ });
+ });
+} else {
+ // Hides card fields if the merchant isn't eligible
+ document.querySelector("#card-form").style = "display: none";
+}
+
+// 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/advanced-integration/beta/package.json b/advanced-integration/beta/package.json
new file mode 100644
index 00000000..c87347ac
--- /dev/null
+++ b/advanced-integration/beta/package.json
@@ -0,0 +1,23 @@
+{
+ "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/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",
+ "express": "^4.18.2",
+ "node-fetch": "^3.3.2"
+ },
+ "devDependencies": {
+ "nodemon": "^3.0.1"
+ }
+}
diff --git a/advanced-integration/beta/server/server.js b/advanced-integration/beta/server/server.js
new file mode 100644
index 00000000..0d8d3cb8
--- /dev/null
+++ b/advanced-integration/beta/server/server.js
@@ -0,0 +1,152 @@
+import express from "express";
+import fetch from "node-fetch";
+import "dotenv/config";
+import path from "path";
+
+const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PORT = 8888 } = process.env;
+const base = "https://api-m.sandbox.paypal.com";
+const app = express();
+
+// 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 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);
+ }
+};
+
+/**
+ * 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);
+ }
+}
+
+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." });
+ }
+});
+
+// serve index.html
+app.get("/", (req, res) => {
+ res.sendFile(path.resolve("./client/checkout.html"));
+});
+
+app.listen(PORT, () => {
+ console.log(`Node server listening at http://localhost:${PORT}/`);
+});