From 55a452b4d668aeb1ed92624e36911da8da2cdc79 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Wed, 1 Feb 2023 10:55:40 -0600 Subject: [PATCH 01/53] chore: update createOrder callback comment and example payload (#21) --- advanced-integration/public/app.js | 26 +++++++++++++++++++++----- standard-integration/public/index.html | 14 +++++++++++--- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/advanced-integration/public/app.js b/advanced-integration/public/app.js index 056efc84..0eaef6db 100644 --- a/advanced-integration/public/app.js +++ b/advanced-integration/public/app.js @@ -1,17 +1,25 @@ paypal .Buttons({ // Sets up the transaction when a payment button is clicked - createOrder: function (data, actions) { + createOrder: function () { return fetch("/api/orders", { method: "post", // use the "body" param to optionally pass additional order information - // like product ids or amount + // like product skus and quantities + body: JSON.stringify({ + cart: [ + { + sku: "", + quantity: "", + }, + ], + }), }) .then((response) => response.json()) .then((order) => order.id); }, // Finalize the transaction after payer approval - onApprove: function (data, actions) { + onApprove: function (data) { return fetch(`/api/orders/${data.orderID}/capture`, { method: "post", }) @@ -47,8 +55,16 @@ if (paypal.HostedFields.isEligible()) { createOrder: () => { return fetch("/api/orders", { method: "post", - // use the "body" param to optionally pass additional order information like - // product ids or amount. + // use the "body" param to optionally pass additional order information + // like product skus and quantities + body: JSON.stringify({ + cart: [ + { + sku: "", + quantity: "", + }, + ], + }), }) .then((res) => res.json()) .then((orderData) => { diff --git a/standard-integration/public/index.html b/standard-integration/public/index.html index f0f8b659..28dba802 100644 --- a/standard-integration/public/index.html +++ b/standard-integration/public/index.html @@ -12,17 +12,25 @@ paypal .Buttons({ // Sets up the transaction when a payment button is clicked - createOrder: function (data, actions) { + createOrder: function () { return fetch("/api/orders", { method: "post", // use the "body" param to optionally pass additional order information - // like product ids or amount + // like product skus and quantities + body: JSON.stringify({ + cart: [ + { + sku: "", + quantity: "", + }, + ], + }), }) .then((response) => response.json()) .then((order) => order.id); }, // Finalize the transaction after payer approval - onApprove: function (data, actions) { + onApprove: function (data) { return fetch(`/api/orders/${data.orderID}/capture`, { method: "post", }) From 4ce5e89a3ff32ababef67a8f32f30cd4e6a67d28 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Thu, 16 Feb 2023 14:13:33 -0600 Subject: [PATCH 02/53] chore(standard-integration): update fetch calls to match public docs (#23) --- standard-integration/public/index.html | 20 ++++++++++++++------ standard-integration/server.js | 9 ++++++--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/standard-integration/public/index.html b/standard-integration/public/index.html index 28dba802..7abaf8cc 100644 --- a/standard-integration/public/index.html +++ b/standard-integration/public/index.html @@ -13,15 +13,18 @@ .Buttons({ // Sets up the transaction when a payment button is clicked createOrder: function () { - return fetch("/api/orders", { + return fetch("/my-server/create-paypal-order", { method: "post", + headers: { + "Content-Type": "application/json", + }, // use the "body" param to optionally pass additional order information // like product skus and quantities body: JSON.stringify({ cart: [ { - sku: "", - quantity: "", + sku: "YOUR_PRODUCT_STOCK_KEEPING_UNIT", + quantity: "YOUR_PRODUCT_QUANTITY", }, ], }), @@ -31,8 +34,14 @@ }, // Finalize the transaction after payer approval onApprove: function (data) { - return fetch(`/api/orders/${data.orderID}/capture`, { + return fetch("/my-server/capture-paypal-order", { method: "post", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + orderID: data.orderID, + }), }) .then((response) => response.json()) .then((orderData) => { @@ -42,8 +51,7 @@ orderData, JSON.stringify(orderData, null, 2) ); - var transaction = - orderData.purchase_units[0].payments.captures[0]; + const transaction = orderData.purchase_units[0].payments.captures[0]; alert( "Transaction " + transaction.status + diff --git a/standard-integration/server.js b/standard-integration/server.js index d9d40db4..27bce19e 100644 --- a/standard-integration/server.js +++ b/standard-integration/server.js @@ -6,7 +6,10 @@ const app = express(); app.use(express.static("public")); -app.post("/api/orders", async (req, res) => { +// parse post params sent in body in json format +app.use(express.json()); + +app.post("/my-server/create-paypal-order", async (req, res) => { try { const order = await paypal.createOrder(); res.json(order); @@ -15,8 +18,8 @@ app.post("/api/orders", async (req, res) => { } }); -app.post("/api/orders/:orderID/capture", async (req, res) => { - const { orderID } = req.params; +app.post("/my-server/capture-paypal-order", async (req, res) => { + const { orderID } = req.body; try { const captureData = await paypal.capturePayment(orderID); res.json(captureData); From 603f3a91f7b9963c336d3484803d09b6b81a00c0 Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Thu, 16 Feb 2023 16:24:38 -0600 Subject: [PATCH 03/53] docs: main readme enhancements (#25) --- README.md | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e8bbac74..a3d1c9f5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,41 @@ # PayPal Developer Docs Example Code -Examples from the official PayPal Developer Docs + +Examples from the official [PayPal Developer Docs](https://developer.paypal.com/). + +## Introduction and Overview + +This repository contains two directories: + +- [Standard integration](./standard-integration/) + - Set up standard payments on your checkout page for your buyers. +- [Advanced integration](./advanced-integration/) + - Build and customize a card payment form to accept debit and credit cards. + +**Not sure where to start?** Choose the [standard integration](./standard-integration/). + +### The PayPal JavaScript SDK + +These examples use the [PayPal JavaScript SDK](https://developer.paypal.com/sdk/js/) to display PayPal supported payment methods and provide a seamless checkout experience for your buyers. + +The SDK has several [configuration options](https://developer.paypal.com/sdk/js/configuration/) available. The examples in this repository provide the most minimal example possible to complete a successful transaction. + +## Know before you code + +### Setup a PayPal Account + +To get started with standard checkout, you'll need a developer, personal, or business account. + +[Sign Up](https://www.paypal.com/signin/client?flow=provisionUser) or [Log In](https://www.paypal.com/signin?returnUri=https%253A%252F%252Fdeveloper.paypal.com%252Fdeveloper%252Fapplications&intent=developer) + +You'll then need to visit the [Developer Dashboard](https://developer.paypal.com/dashboard/) to obtain credentials and to +make sandbox accounts. + +### Create an Application + +Once you've setup a PayPal account, you'll need to obtain a **Client ID** and **Secret**. [Create a sandbox application](https://developer.paypal.com/dashboard/applications/sandbox/create). + +### Have Node.js installed + +These examples will ask you to run commands like `npm install` and `npm start`. + +You'll need a version of node >= 14 which can be downloaded from the [Node.js website](https://nodejs.org/en/download/). \ No newline at end of file From a16cf30118da457008edae2275a8be070bfe3496 Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Thu, 16 Feb 2023 16:44:30 -0600 Subject: [PATCH 04/53] chore: improve .env instructions (#27) --- advanced-integration/.env | 2 -- advanced-integration/.env.example | 5 +++++ advanced-integration/.gitignore | 1 + advanced-integration/README.md | 2 +- standard-integration/.env | 2 -- standard-integration/.env.example | 5 +++++ standard-integration/.gitignore | 1 + standard-integration/README.md | 4 ++-- 8 files changed, 15 insertions(+), 7 deletions(-) delete mode 100644 advanced-integration/.env create mode 100644 advanced-integration/.env.example create mode 100644 advanced-integration/.gitignore delete mode 100644 standard-integration/.env create mode 100644 standard-integration/.env.example create mode 100644 standard-integration/.gitignore diff --git a/advanced-integration/.env b/advanced-integration/.env deleted file mode 100644 index f72af4c7..00000000 --- a/advanced-integration/.env +++ /dev/null @@ -1,2 +0,0 @@ -CLIENT_ID= -APP_SECRET= \ No newline at end of file diff --git a/advanced-integration/.env.example b/advanced-integration/.env.example new file mode 100644 index 00000000..ed50f9b7 --- /dev/null +++ b/advanced-integration/.env.example @@ -0,0 +1,5 @@ +# Create an application to obtain credentials at +# https://developer.paypal.com/dashboard/applications/sandbox + +CLIENT_ID="YOUR_CLIENT_ID_GOES_HERE" +APP_SECRET="YOUR_SECRET_GOES_HERE" diff --git a/advanced-integration/.gitignore b/advanced-integration/.gitignore new file mode 100644 index 00000000..4c49bd78 --- /dev/null +++ b/advanced-integration/.gitignore @@ -0,0 +1 @@ +.env diff --git a/advanced-integration/README.md b/advanced-integration/README.md index d282c7c3..509315bc 100644 --- a/advanced-integration/README.md +++ b/advanced-integration/README.md @@ -2,7 +2,7 @@ ## Instructions -1. Add `CLIENT_ID` and `APP_SECRET` to the `.env` file +1. Rename `.env.example` to `.env` and update `CLIENT_ID` and `APP_SECRET`. 2. Run `npm install` 3. Run `npm start` 4. Open http://localhost:8888 diff --git a/standard-integration/.env b/standard-integration/.env deleted file mode 100644 index f72af4c7..00000000 --- a/standard-integration/.env +++ /dev/null @@ -1,2 +0,0 @@ -CLIENT_ID= -APP_SECRET= \ No newline at end of file diff --git a/standard-integration/.env.example b/standard-integration/.env.example new file mode 100644 index 00000000..ed50f9b7 --- /dev/null +++ b/standard-integration/.env.example @@ -0,0 +1,5 @@ +# Create an application to obtain credentials at +# https://developer.paypal.com/dashboard/applications/sandbox + +CLIENT_ID="YOUR_CLIENT_ID_GOES_HERE" +APP_SECRET="YOUR_SECRET_GOES_HERE" diff --git a/standard-integration/.gitignore b/standard-integration/.gitignore new file mode 100644 index 00000000..4c49bd78 --- /dev/null +++ b/standard-integration/.gitignore @@ -0,0 +1 @@ +.env diff --git a/standard-integration/README.md b/standard-integration/README.md index 7220aea8..b40aee92 100644 --- a/standard-integration/README.md +++ b/standard-integration/README.md @@ -5,9 +5,9 @@ This folder contains example code for a standard PayPal integration using both t ## Instructions 1. [Create an application](https://developer.paypal.com/dashboard/applications/sandbox/create) -3. Add your app's `CLIENT_ID` and `APP_SECRET` to the `.env` file +3. Rename `.env.example` to `.env` and update `CLIENT_ID` and `APP_SECRET` 2. Replace `test` in `public/index.html` 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). +7. Click "PayPal" and log in with one of your [Sandbox test accounts](https://developer.paypal.com/dashboard/accounts) From b15a93633d580ebd599812c285129ec435c3f0e3 Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Fri, 17 Feb 2023 09:55:54 -0600 Subject: [PATCH 05/53] chore: log URL with port on app start (#26) --- advanced-integration/server.js | 5 ++++- standard-integration/server.js | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/advanced-integration/server.js b/advanced-integration/server.js index e8069220..ab86bad0 100644 --- a/advanced-integration/server.js +++ b/advanced-integration/server.js @@ -1,6 +1,7 @@ import "dotenv/config"; import express from "express"; import * as paypal from "./paypal-api.js"; +const {PORT = 8888} = process.env; const app = express(); app.set("view engine", "ejs"); @@ -38,4 +39,6 @@ app.post("/api/orders/:orderID/capture", async (req, res) => { } }); -app.listen(8888); +app.listen(PORT, () => { + console.log(`Server listening at http://localhost:${PORT}/`); +}); diff --git a/standard-integration/server.js b/standard-integration/server.js index 27bce19e..d39f9aa0 100644 --- a/standard-integration/server.js +++ b/standard-integration/server.js @@ -1,6 +1,7 @@ import "dotenv/config"; // loads variables from .env file import express from "express"; import * as paypal from "./paypal-api.js"; +const {PORT = 8888} = process.env; const app = express(); @@ -28,4 +29,6 @@ app.post("/my-server/capture-paypal-order", async (req, res) => { } }); -app.listen(8888); +app.listen(PORT, () => { + console.log(`Server listening at http://localhost:${PORT}/`); +}); From 5dd4bce83ddf2e3e217e300f369301b1a7331393 Mon Sep 17 00:00:00 2001 From: cnallam <130782580+cnallam@users.noreply.github.com> Date: Thu, 22 Jun 2023 11:50:30 -0700 Subject: [PATCH 06/53] feat: add support for GitHub Codespaces (#52) --- .../advanced-integration/devcontainer.json | 50 +++++++++++++++++++ .../advanced-integration/welcome-message.sh | 23 +++++++++ .../standard-integration/devcontainer.json | 50 +++++++++++++++++++ .../standard-integration/welcome-message.sh | 23 +++++++++ 4 files changed, 146 insertions(+) create mode 100644 .devcontainer/advanced-integration/devcontainer.json create mode 100644 .devcontainer/advanced-integration/welcome-message.sh create mode 100644 .devcontainer/standard-integration/devcontainer.json create mode 100644 .devcontainer/standard-integration/welcome-message.sh diff --git a/.devcontainer/advanced-integration/devcontainer.json b/.devcontainer/advanced-integration/devcontainer.json new file mode 100644 index 00000000..04906d1a --- /dev/null +++ b/.devcontainer/advanced-integration/devcontainer.json @@ -0,0 +1,50 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "PayPal Advanced Integration", + "image": "mcr.microsoft.com/devcontainers/universal:2", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration", + + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash ../.devcontainer/advanced-integration/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": { + "CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "APP_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/welcome-message.sh b/.devcontainer/advanced-integration/welcome-message.sh new file mode 100644 index 00000000..a37ec162 --- /dev/null +++ b/.devcontainer/advanced-integration/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 \"CLIENT_ID\" and \"APP_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 "$CLIENT_ID" ] && [ -n "$APP_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/.devcontainer/standard-integration/devcontainer.json b/.devcontainer/standard-integration/devcontainer.json new file mode 100644 index 00000000..dd03d372 --- /dev/null +++ b/.devcontainer/standard-integration/devcontainer.json @@ -0,0 +1,50 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "PayPal Standard Integration", + "image": "mcr.microsoft.com/devcontainers/universal:2", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration", + + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash ../.devcontainer/standard-integration/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": { + "CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "APP_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/standard-integration/welcome-message.sh b/.devcontainer/standard-integration/welcome-message.sh new file mode 100644 index 00000000..debc7864 --- /dev/null +++ b/.devcontainer/standard-integration/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"CLIENT_ID\" and \"APP_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard 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 "$CLIENT_ID" ] && [ -n "$APP_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" From 08066355a73b00eb29d0053c2bd256e26d8a8e1c Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Mon, 24 Jul 2023 09:49:30 -0500 Subject: [PATCH 07/53] chore: use es6 syntax to match public docs (#59) --- standard-integration/public/index.html | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/standard-integration/public/index.html b/standard-integration/public/index.html index 7abaf8cc..7ba43047 100644 --- a/standard-integration/public/index.html +++ b/standard-integration/public/index.html @@ -12,9 +12,9 @@ paypal .Buttons({ // Sets up the transaction when a payment button is clicked - createOrder: function () { + createOrder() { return fetch("/my-server/create-paypal-order", { - method: "post", + method: "POST", headers: { "Content-Type": "application/json", }, @@ -33,9 +33,9 @@ .then((order) => order.id); }, // Finalize the transaction after payer approval - onApprove: function (data) { + onApprove(data) { return fetch("/my-server/capture-paypal-order", { - method: "post", + method: "POST", headers: { "Content-Type": "application/json", }, @@ -52,13 +52,7 @@ JSON.stringify(orderData, null, 2) ); const transaction = orderData.purchase_units[0].payments.captures[0]; - alert( - "Transaction " + - transaction.status + - ": " + - transaction.id + - "\n\nSee console for all available details" - ); + alert(`Transaction ${transaction.status}: ${transaction.id}\n\nSee console for all available details`); // When ready to go live, remove the alert and show a success message within this page. For example: // var element = document.getElementById('paypal-button-container'); // element.innerHTML = '

Thank you for your payment!

'; From 3d333d562180631bc7383c5ca8087c79f9f561c9 Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Fri, 28 Jul 2023 07:59:28 -0500 Subject: [PATCH 08/53] add JSDoc links to REST APIs (#28) * add JSDoc links to REST APIs Co-authored-by: Greg Jopa <534034+gregjopa@users.noreply.github.com> --- advanced-integration/paypal-api.js | 20 ++++++++++++++++---- standard-integration/paypal-api.js | 12 ++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/advanced-integration/paypal-api.js b/advanced-integration/paypal-api.js index 082e2630..1c9f14b1 100644 --- a/advanced-integration/paypal-api.js +++ b/advanced-integration/paypal-api.js @@ -4,7 +4,10 @@ import fetch from "node-fetch"; const { CLIENT_ID, APP_SECRET } = process.env; const base = "https://api-m.sandbox.paypal.com"; -// call the create order method +/** + * Create an order + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create + */ export async function createOrder() { const purchaseAmount = "100.00"; // TODO: pull prices from a database const accessToken = await generateAccessToken(); @@ -31,7 +34,10 @@ export async function createOrder() { return handleResponse(response); } -// capture payment for an order +/** + * Capture payment for an order + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture + */ export async function capturePayment(orderId) { const accessToken = await generateAccessToken(); const url = `${base}/v2/checkout/orders/${orderId}/capture`; @@ -46,7 +52,10 @@ export async function capturePayment(orderId) { return handleResponse(response); } -// generate access token +/** + * Generate an OAuth 2.0 access token + * @see https://developer.paypal.com/api/rest/authentication/ + */ export async function generateAccessToken() { const auth = Buffer.from(CLIENT_ID + ":" + APP_SECRET).toString("base64"); const response = await fetch(`${base}/v1/oauth2/token`, { @@ -60,7 +69,10 @@ export async function generateAccessToken() { return jsonData.access_token; } -// generate client token +/** + * Generate a client token + * @see https://developer.paypal.com/docs/checkout/advanced/integrate/#link-sampleclienttokenrequest + */ export async function generateClientToken() { const accessToken = await generateAccessToken(); const response = await fetch(`${base}/v1/identity/generate-token`, { diff --git a/standard-integration/paypal-api.js b/standard-integration/paypal-api.js index 35cca914..998102ca 100644 --- a/standard-integration/paypal-api.js +++ b/standard-integration/paypal-api.js @@ -3,6 +3,10 @@ import fetch from "node-fetch"; const { CLIENT_ID, APP_SECRET } = process.env; const base = "https://api-m.sandbox.paypal.com"; +/** + * Create an order + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create + */ export async function createOrder() { const accessToken = await generateAccessToken(); const url = `${base}/v2/checkout/orders`; @@ -28,6 +32,10 @@ export async function createOrder() { return handleResponse(response); } +/** + * Capture payment for an order + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture + */ export async function capturePayment(orderId) { const accessToken = await generateAccessToken(); const url = `${base}/v2/checkout/orders/${orderId}/capture`; @@ -42,6 +50,10 @@ export async function capturePayment(orderId) { return handleResponse(response); } +/** + * Generate an OAuth 2.0 access token + * @see https://developer.paypal.com/api/rest/authentication/ + */ export async function generateAccessToken() { const auth = Buffer.from(CLIENT_ID + ":" + APP_SECRET).toString("base64"); const response = await fetch(`${base}/v1/oauth2/token`, { From e859c6f49a0e3c403b31b895a81f860e4b706cf5 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Tue, 15 Aug 2023 09:56:52 -0500 Subject: [PATCH 09/53] Refactor the standard integration guide (#61) * Refactor the standard integration guide * chore: pr feedback - read port from env var * chore(docs): recommend at least Node 16 * chore: update deps and server startup message * fix: update the codespaces path for the standard integration --- .../standard-integration/devcontainer.json | 8 +- README.md | 2 +- standard-integration/.env.example | 4 +- standard-integration/README.md | 2 +- standard-integration/index.html | 119 ++++++++++++++ standard-integration/package.json | 6 +- standard-integration/paypal-api.js | 78 --------- standard-integration/public/index.html | 66 -------- standard-integration/server.js | 151 ++++++++++++++++-- 9 files changed, 264 insertions(+), 172 deletions(-) create mode 100644 standard-integration/index.html delete mode 100644 standard-integration/paypal-api.js delete mode 100644 standard-integration/public/index.html diff --git a/.devcontainer/standard-integration/devcontainer.json b/.devcontainer/standard-integration/devcontainer.json index dd03d372..22576145 100644 --- a/.devcontainer/standard-integration/devcontainer.json +++ b/.devcontainer/standard-integration/devcontainer.json @@ -2,7 +2,7 @@ { "name": "PayPal Standard Integration", "image": "mcr.microsoft.com/devcontainers/universal:2", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/standard-integration", // Use 'onCreateCommand' to run commands when creating the container. "onCreateCommand": "bash ../.devcontainer/standard-integration/welcome-message.sh", @@ -21,17 +21,17 @@ ], "portsAttributes": { "8888": { - "label": "Preview of Advanced Checkout Flow", + "label": "Preview of Standard Checkout Flow", "onAutoForward": "openBrowserOnce" } }, "secrets": { - "CLIENT_ID": { + "PAYPAL_CLIENT_ID": { "description": "Sandbox client ID of the application.", "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" }, - "APP_SECRET": { + "PAYPAL_CLIENT_SECRET": { "description": "Sandbox secret of the application.", "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" } diff --git a/README.md b/README.md index a3d1c9f5..d37a5e98 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,4 @@ Once you've setup a PayPal account, you'll need to obtain a **Client ID** and ** These examples will ask you to run commands like `npm install` and `npm start`. -You'll need a version of node >= 14 which can be downloaded from the [Node.js website](https://nodejs.org/en/download/). \ No newline at end of file +You'll need a version of node >= 16 which can be downloaded from the [Node.js website](https://nodejs.org/en/download/). \ No newline at end of file diff --git a/standard-integration/.env.example b/standard-integration/.env.example index ed50f9b7..0fb8a60a 100644 --- a/standard-integration/.env.example +++ b/standard-integration/.env.example @@ -1,5 +1,5 @@ # Create an application to obtain credentials at # https://developer.paypal.com/dashboard/applications/sandbox -CLIENT_ID="YOUR_CLIENT_ID_GOES_HERE" -APP_SECRET="YOUR_SECRET_GOES_HERE" +PAYPAL_CLIENT_ID="YOUR_CLIENT_ID_GOES_HERE" +PAYPAL_CLIENT_SECRET="YOUR_SECRET_GOES_HERE" diff --git a/standard-integration/README.md b/standard-integration/README.md index b40aee92..969b8fa3 100644 --- a/standard-integration/README.md +++ b/standard-integration/README.md @@ -5,7 +5,7 @@ This folder contains example code for a standard PayPal integration using both t ## Instructions 1. [Create an application](https://developer.paypal.com/dashboard/applications/sandbox/create) -3. Rename `.env.example` to `.env` and update `CLIENT_ID` and `APP_SECRET` +3. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET` 2. Replace `test` in `public/index.html` with your app's client-id 4. Run `npm install` 5. Run `npm start` diff --git a/standard-integration/index.html b/standard-integration/index.html new file mode 100644 index 00000000..a8baf73f --- /dev/null +++ b/standard-integration/index.html @@ -0,0 +1,119 @@ + + + + + + PayPal JS SDK Standard Integration + + +
+ + + + + diff --git a/standard-integration/package.json b/standard-integration/package.json index 766860ec..c8f4c157 100644 --- a/standard-integration/package.json +++ b/standard-integration/package.json @@ -11,8 +11,8 @@ "license": "Apache-2.0", "description": "", "dependencies": { - "dotenv": "^16.0.0", - "express": "^4.17.3", - "node-fetch": "^3.2.1" + "dotenv": "^16.3.1", + "express": "^4.18.2", + "node-fetch": "^3.3.2" } } diff --git a/standard-integration/paypal-api.js b/standard-integration/paypal-api.js deleted file mode 100644 index 998102ca..00000000 --- a/standard-integration/paypal-api.js +++ /dev/null @@ -1,78 +0,0 @@ -import fetch from "node-fetch"; - -const { CLIENT_ID, APP_SECRET } = process.env; -const base = "https://api-m.sandbox.paypal.com"; - -/** - * Create an order - * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create - */ -export async function createOrder() { - const accessToken = await generateAccessToken(); - const url = `${base}/v2/checkout/orders`; - const response = await fetch(url, { - method: "post", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - intent: "CAPTURE", - purchase_units: [ - { - amount: { - currency_code: "USD", - value: "100.00", - }, - }, - ], - }), - }); - - return handleResponse(response); -} - -/** - * Capture payment for an order - * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture - */ -export async function capturePayment(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}`, - }, - }); - - return handleResponse(response); -} - -/** - * Generate an OAuth 2.0 access token - * @see https://developer.paypal.com/api/rest/authentication/ - */ -export async function generateAccessToken() { - const auth = Buffer.from(CLIENT_ID + ":" + APP_SECRET).toString("base64"); - const response = await fetch(`${base}/v1/oauth2/token`, { - method: "post", - body: "grant_type=client_credentials", - headers: { - Authorization: `Basic ${auth}`, - }, - }); - - const jsonData = await handleResponse(response); - return jsonData.access_token; -} - -async function handleResponse(response) { - if (response.status === 200 || response.status === 201) { - return response.json(); - } - - const errorMessage = await response.text(); - throw new Error(errorMessage); -} diff --git a/standard-integration/public/index.html b/standard-integration/public/index.html deleted file mode 100644 index 7ba43047..00000000 --- a/standard-integration/public/index.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - -
- - - diff --git a/standard-integration/server.js b/standard-integration/server.js index d39f9aa0..87826f70 100644 --- a/standard-integration/server.js +++ b/standard-integration/server.js @@ -1,34 +1,151 @@ -import "dotenv/config"; // loads variables from .env file -import express from "express"; -import * as paypal from "./paypal-api.js"; -const {PORT = 8888} = process.env; +import express from 'express'; +import fetch from 'node-fetch'; +import 'dotenv/config'; +import path from 'path'; +const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET } = process.env; +const base = 'https://api-m.sandbox.paypal.com'; const app = express(); -app.use(express.static("public")); - // parse post params sent in body in json format app.use(express.json()); -app.post("/my-server/create-paypal-order", async (req, res) => { +/** + * 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 { - const order = await paypal.createOrder(); - res.json(order); + 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: '0.02', + }, + }, + ], + }; + + 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) { - res.status(500).send(err.message); + 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("/my-server/capture-paypal-order", async (req, res) => { - const { orderID } = req.body; +app.post('/api/orders/:orderID/capture', async (req, res) => { try { - const captureData = await paypal.capturePayment(orderID); - res.json(captureData); - } catch (err) { - res.status(500).send(err.message); + 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('./index.html')); +}); + +const PORT = Number(process.env.PORT) || 8888; + app.listen(PORT, () => { - console.log(`Server listening at http://localhost:${PORT}/`); + console.log(`Node server listening at http://localhost:${PORT}/`); }); From dabcc1e41cce327d6f2aedd06eaef23ac6057867 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Tue, 15 Aug 2023 10:00:20 -0500 Subject: [PATCH 10/53] chore(docs): fix path to index.html in readme (#62) --- standard-integration/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard-integration/README.md b/standard-integration/README.md index 969b8fa3..829911be 100644 --- a/standard-integration/README.md +++ b/standard-integration/README.md @@ -6,7 +6,7 @@ This folder contains example code for a standard PayPal integration using both t 1. [Create an application](https://developer.paypal.com/dashboard/applications/sandbox/create) 3. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET` -2. Replace `test` in `public/index.html` with your app's client-id +2. Replace `test` in `index.html` with your app's client-id 4. Run `npm install` 5. Run `npm start` 6. Open http://localhost:8888 From ed5eb63404ee6a2f9c59e1b62856515dded8535a Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Tue, 15 Aug 2023 10:31:35 -0500 Subject: [PATCH 11/53] Standardize license format (#63) --- LICENSE | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/LICENSE b/LICENSE index 261eeb9e..0699f06b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -175,24 +175,13 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] + Copyright 2022 PayPal Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, From 9363b5c610ad5bc55f5fcf57d5d46bc547b458f1 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Wed, 16 Aug 2023 12:34:55 -0500 Subject: [PATCH 12/53] Refactor the backend for the advanced integration guide (#64) --- .../advanced-integration/devcontainer.json | 4 +- advanced-integration/.env.example | 4 +- advanced-integration/README.md | 2 +- advanced-integration/package.json | 10 +- advanced-integration/paypal-api.js | 98 ---------- advanced-integration/public/app.js | 20 +- advanced-integration/server.js | 182 +++++++++++++++--- standard-integration/package.json | 4 +- standard-integration/server.js | 4 +- 9 files changed, 187 insertions(+), 141 deletions(-) delete mode 100644 advanced-integration/paypal-api.js diff --git a/.devcontainer/advanced-integration/devcontainer.json b/.devcontainer/advanced-integration/devcontainer.json index 04906d1a..9b4e4d0d 100644 --- a/.devcontainer/advanced-integration/devcontainer.json +++ b/.devcontainer/advanced-integration/devcontainer.json @@ -27,11 +27,11 @@ }, "secrets": { - "CLIENT_ID": { + "PAYPAL_CLIENT_ID": { "description": "Sandbox client ID of the application.", "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" }, - "APP_SECRET": { + "PAYPAL_CLIENT_SECRET": { "description": "Sandbox secret of the application.", "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" } diff --git a/advanced-integration/.env.example b/advanced-integration/.env.example index ed50f9b7..0fb8a60a 100644 --- a/advanced-integration/.env.example +++ b/advanced-integration/.env.example @@ -1,5 +1,5 @@ # Create an application to obtain credentials at # https://developer.paypal.com/dashboard/applications/sandbox -CLIENT_ID="YOUR_CLIENT_ID_GOES_HERE" -APP_SECRET="YOUR_SECRET_GOES_HERE" +PAYPAL_CLIENT_ID="YOUR_CLIENT_ID_GOES_HERE" +PAYPAL_CLIENT_SECRET="YOUR_SECRET_GOES_HERE" diff --git a/advanced-integration/README.md b/advanced-integration/README.md index 509315bc..b81b9de4 100644 --- a/advanced-integration/README.md +++ b/advanced-integration/README.md @@ -2,7 +2,7 @@ ## Instructions -1. Rename `.env.example` to `.env` and update `CLIENT_ID` and `APP_SECRET`. +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 diff --git a/advanced-integration/package.json b/advanced-integration/package.json index cc028060..1b99d8b3 100644 --- a/advanced-integration/package.json +++ b/advanced-integration/package.json @@ -1,5 +1,5 @@ { - "name": "@paypalcorp/advanced-integration", + "name": "paypal-advanced-integration", "version": "1.0.0", "description": "", "main": "server.js", @@ -11,9 +11,9 @@ "author": "", "license": "Apache-2.0", "dependencies": { - "dotenv": "^16.0.0", - "ejs": "^3.1.6", - "express": "^4.17.3", - "node-fetch": "^3.2.1" + "dotenv": "^16.3.1", + "ejs": "^3.1.9", + "express": "^4.18.2", + "node-fetch": "^3.3.2" } } diff --git a/advanced-integration/paypal-api.js b/advanced-integration/paypal-api.js deleted file mode 100644 index 1c9f14b1..00000000 --- a/advanced-integration/paypal-api.js +++ /dev/null @@ -1,98 +0,0 @@ -import fetch from "node-fetch"; - -// set some important variables -const { CLIENT_ID, APP_SECRET } = process.env; -const base = "https://api-m.sandbox.paypal.com"; - -/** - * Create an order - * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create - */ -export async function createOrder() { - const purchaseAmount = "100.00"; // TODO: pull prices from a database - const accessToken = await generateAccessToken(); - const url = `${base}/v2/checkout/orders`; - const response = await fetch(url, { - method: "post", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - intent: "CAPTURE", - purchase_units: [ - { - amount: { - currency_code: "USD", - value: purchaseAmount, - }, - }, - ], - }), - }); - - return handleResponse(response); -} - -/** - * Capture payment for an order - * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture - */ -export async function capturePayment(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}`, - }, - }); - - return handleResponse(response); -} - -/** - * Generate an OAuth 2.0 access token - * @see https://developer.paypal.com/api/rest/authentication/ - */ -export async function generateAccessToken() { - const auth = Buffer.from(CLIENT_ID + ":" + APP_SECRET).toString("base64"); - const response = await fetch(`${base}/v1/oauth2/token`, { - method: "post", - body: "grant_type=client_credentials", - headers: { - Authorization: `Basic ${auth}`, - }, - }); - const jsonData = await handleResponse(response); - return jsonData.access_token; -} - -/** - * Generate a client token - * @see https://developer.paypal.com/docs/checkout/advanced/integrate/#link-sampleclienttokenrequest - */ -export async function generateClientToken() { - const accessToken = await generateAccessToken(); - const response = await fetch(`${base}/v1/identity/generate-token`, { - method: "post", - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Language": "en_US", - "Content-Type": "application/json", - }, - }); - console.log('response', response.status) - const jsonData = await handleResponse(response); - return jsonData.client_token; -} - -async function handleResponse(response) { - if (response.status === 200 || response.status === 201) { - return response.json(); - } - - const errorMessage = await response.text(); - throw new Error(errorMessage); -} diff --git a/advanced-integration/public/app.js b/advanced-integration/public/app.js index 0eaef6db..6d0c3e14 100644 --- a/advanced-integration/public/app.js +++ b/advanced-integration/public/app.js @@ -3,7 +3,10 @@ paypal // Sets up the transaction when a payment button is clicked createOrder: function () { return fetch("/api/orders", { - method: "post", + method: "POST", + headers: { + 'Content-Type': 'application/json', + }, // use the "body" param to optionally pass additional order information // like product skus and quantities body: JSON.stringify({ @@ -21,7 +24,10 @@ paypal // Finalize the transaction after payer approval onApprove: function (data) { return fetch(`/api/orders/${data.orderID}/capture`, { - method: "post", + method: "POST", + headers: { + 'Content-Type': 'application/json', + }, }) .then((response) => response.json()) .then((orderData) => { @@ -54,7 +60,10 @@ if (paypal.HostedFields.isEligible()) { // Call your server to set up the transaction createOrder: () => { return fetch("/api/orders", { - method: "post", + method: "POST", + headers: { + 'Content-Type': 'application/json', + }, // use the "body" param to optionally pass additional order information // like product skus and quantities body: JSON.stringify({ @@ -127,7 +136,10 @@ if (paypal.HostedFields.isEligible()) { }) .then(() => { fetch(`/api/orders/${orderId}/capture`, { - method: "post", + method: "POST", + headers: { + 'Content-Type': 'application/json', + }, }) .then((res) => res.json()) .then((orderData) => { diff --git a/advanced-integration/server.js b/advanced-integration/server.js index ab86bad0..fb2d8c35 100644 --- a/advanced-integration/server.js +++ b/advanced-integration/server.js @@ -1,44 +1,178 @@ -import "dotenv/config"; -import express from "express"; -import * as paypal from "./paypal-api.js"; -const {PORT = 8888} = process.env; +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(); -app.set("view engine", "ejs"); -app.use(express.static("public")); +app.set('view engine', 'ejs'); +app.use(express.static('public')); + +// 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: '0.02', + }, + }, + ], + }; + + 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) => { - const clientId = process.env.CLIENT_ID; +app.get('/', async (req, res) => { try { - const clientToken = await paypal.generateClientToken(); - res.render("checkout", { clientId, clientToken }); + const { jsonResponse } = await generateClientToken(); + res.render('checkout', { + clientId: PAYPAL_CLIENT_ID, + clientToken: jsonResponse.client_token, + }); } catch (err) { res.status(500).send(err.message); } }); -// create order -app.post("/api/orders", async (req, res) => { +app.post('/api/orders', async (req, res) => { try { - const order = await paypal.createOrder(); - res.json(order); - } catch (err) { - res.status(500).send(err.message); + // 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.' }); } }); -// capture payment -app.post("/api/orders/:orderID/capture", async (req, res) => { - const { orderID } = req.params; +app.post('/api/orders/:orderID/capture', async (req, res) => { try { - const captureData = await paypal.capturePayment(orderID); - res.json(captureData); - } catch (err) { - res.status(500).send(err.message); + 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(`Server listening at http://localhost:${PORT}/`); + console.log(`Node server listening at http://localhost:${PORT}/`); }); diff --git a/standard-integration/package.json b/standard-integration/package.json index c8f4c157..86974e31 100644 --- a/standard-integration/package.json +++ b/standard-integration/package.json @@ -1,7 +1,7 @@ { - "name": "@paypalcorp/standard-integration", + "name": "paypal-standard-integration", "version": "1.0.0", - "main": "paypal-api.js", + "main": "server.js", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", diff --git a/standard-integration/server.js b/standard-integration/server.js index 87826f70..7b7310f0 100644 --- a/standard-integration/server.js +++ b/standard-integration/server.js @@ -3,7 +3,7 @@ import fetch from 'node-fetch'; import 'dotenv/config'; import path from 'path'; -const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET } = process.env; +const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PORT = 8888 } = process.env; const base = 'https://api-m.sandbox.paypal.com'; const app = express(); @@ -144,8 +144,6 @@ app.get('/', (req, res) => { res.sendFile(path.resolve('./index.html')); }); -const PORT = Number(process.env.PORT) || 8888; - app.listen(PORT, () => { console.log(`Node server listening at http://localhost:${PORT}/`); }); From 5a9a93114eae3c4f8b2d9182ce782c8a952adf06 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Thu, 17 Aug 2023 10:41:24 -0500 Subject: [PATCH 13/53] Use a separate container for the result message (#65) --- standard-integration/index.html | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/standard-integration/index.html b/standard-integration/index.html index a8baf73f..d22c1d1f 100644 --- a/standard-integration/index.html +++ b/standard-integration/index.html @@ -7,12 +7,13 @@
+

From d84f10cf861449e2434672b2ea465a0176b6e6e3 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Thu, 17 Aug 2023 16:55:52 -0500 Subject: [PATCH 14/53] Align standard and advanced integrations (#66) * Minor tweaks to standard integration * Share JS code between card fields and buttons * PR feedback - check for card decline use case --- advanced-integration/README.md | 4 +- advanced-integration/package.json | 3 +- advanced-integration/public/app.js | 262 +++++++++++++----------- advanced-integration/views/checkout.ejs | 31 +-- standard-integration/README.md | 6 +- standard-integration/index.html | 11 +- standard-integration/package.json | 3 +- 7 files changed, 171 insertions(+), 149 deletions(-) diff --git a/advanced-integration/README.md b/advanced-integration/README.md index b81b9de4..923a5234 100644 --- a/advanced-integration/README.md +++ b/advanced-integration/README.md @@ -1,9 +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) \ No newline at end of file +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/package.json b/advanced-integration/package.json index 1b99d8b3..424f853e 100644 --- a/advanced-integration/package.json +++ b/advanced-integration/package.json @@ -1,14 +1,13 @@ { "name": "paypal-advanced-integration", + "description": "Sample Node.js web app to integrate PayPal Advanced Checkout for online payments", "version": "1.0.0", - "description": "", "main": "server.js", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js" }, - "author": "", "license": "Apache-2.0", "dependencies": { "dotenv": "^16.3.1", diff --git a/advanced-integration/public/app.js b/advanced-integration/public/app.js index 6d0c3e14..b26369c7 100644 --- a/advanced-integration/public/app.js +++ b/advanced-integration/public/app.js @@ -1,172 +1,192 @@ +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]; + + const isHostedFieldsComponent = typeof data.card === 'object'; + + // this actions.restart() behavior only applies to the Buttons component + if ( + errorDetail?.issue === 'INSTRUMENT_DECLINED' && + isHostedFieldsComponent === false + ) { + // (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}`, + ); + } +} + paypal .Buttons({ - // Sets up the transaction when a payment button is clicked - createOrder: function () { - return fetch("/api/orders", { - method: "POST", - headers: { - 'Content-Type': 'application/json', - }, - // use the "body" param to optionally pass additional order information - // like product skus and quantities - body: JSON.stringify({ - cart: [ - { - sku: "", - quantity: "", - }, - ], - }), - }) - .then((response) => response.json()) - .then((order) => order.id); - }, - // Finalize the transaction after payer approval - onApprove: function (data) { - return fetch(`/api/orders/${data.orderID}/capture`, { - method: "POST", - headers: { - 'Content-Type': 'application/json', - }, - }) - .then((response) => response.json()) - .then((orderData) => { - // Successful capture! For dev/demo purposes: - console.log( - "Capture result", - orderData, - JSON.stringify(orderData, null, 2) - ); - const transaction = orderData.purchase_units[0].payments.captures[0]; - alert(`Transaction ${transaction.status}: ${transaction.id} - - See console for all available details - `); - // When ready to go live, remove the alert and show a success message within this page. For example: - // var element = document.getElementById('paypal-button-container'); - // element.innerHTML = '

Thank you for your payment!

'; - // Or go to another URL: actions.redirect('thank_you.html'); - }); - }, + createOrder: createOrderCallback, + onApprove: onApproveCallback, }) - .render("#paypal-button-container"); + .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 (paypal.HostedFields.isEligible()) { - let orderId; - // Renders card fields paypal.HostedFields.render({ // Call your server to set up the transaction - createOrder: () => { - return fetch("/api/orders", { - method: "POST", - headers: { - 'Content-Type': 'application/json', - }, - // use the "body" param to optionally pass additional order information - // like product skus and quantities - body: JSON.stringify({ - cart: [ - { - sku: "", - quantity: "", - }, - ], - }), - }) - .then((res) => res.json()) - .then((orderData) => { - orderId = orderData.id; // needed later to complete capture - return orderData.id; - }); - }, + createOrder: createOrderCallback, styles: { - ".valid": { - color: "green", + '.valid': { + color: 'green', }, - ".invalid": { - color: "red", + '.invalid': { + color: 'red', }, }, fields: { number: { - selector: "#card-number", - placeholder: "4111 1111 1111 1111", + selector: '#card-number', + placeholder: '4111 1111 1111 1111', }, cvv: { - selector: "#cvv", - placeholder: "123", + selector: '#cvv', + placeholder: '123', }, expirationDate: { - selector: "#expiration-date", - placeholder: "MM/YY", + selector: '#expiration-date', + placeholder: 'MM/YY', }, }, }).then((cardFields) => { - document.querySelector("#card-form").addEventListener("submit", (event) => { + document.querySelector('#card-form').addEventListener('submit', (event) => { event.preventDefault(); cardFields .submit({ // Cardholder's first and last name - cardholderName: document.getElementById("card-holder-name").value, + cardholderName: document.getElementById('card-holder-name').value, // Billing Address billingAddress: { // Street address, line 1 streetAddress: document.getElementById( - "card-billing-address-street" + 'card-billing-address-street', ).value, // Street address, line 2 (Ex: Unit, Apartment, etc.) extendedAddress: document.getElementById( - "card-billing-address-unit" + 'card-billing-address-unit', ).value, // State - region: document.getElementById("card-billing-address-state").value, + region: document.getElementById('card-billing-address-state').value, // City - locality: document.getElementById("card-billing-address-city") + locality: document.getElementById('card-billing-address-city') .value, // Postal Code - postalCode: document.getElementById("card-billing-address-zip") + postalCode: document.getElementById('card-billing-address-zip') .value, // Country Code countryCodeAlpha2: document.getElementById( - "card-billing-address-country" + 'card-billing-address-country', ).value, }, }) - .then(() => { - fetch(`/api/orders/${orderId}/capture`, { - method: "POST", - headers: { - 'Content-Type': 'application/json', - }, - }) - .then((res) => res.json()) - .then((orderData) => { - // Two cases to handle: - // (1) Other non-recoverable errors -> Show a failure message - // (2) Successful transaction -> Show confirmation or thank you - // This example reads a v2/checkout/orders capture response, propagated from the server - // You could use a different API or structure for your 'orderData' - const errorDetail = - Array.isArray(orderData.details) && orderData.details[0]; - if (errorDetail) { - var msg = "Sorry, your transaction could not be processed."; - if (errorDetail.description) - msg += "\n\n" + errorDetail.description; - if (orderData.debug_id) msg += " (" + orderData.debug_id + ")"; - return alert(msg); // Show a failure message - } - // Show a success message or redirect - alert("Transaction completed!"); - }); + .then((data) => { + return onApproveCallback(data); }) - .catch((err) => { - alert("Payment could not be captured! " + JSON.stringify(err)); + .catch((orderData) => { + const { links, ...errorMessageData } = orderData; + resultMessage( + `Sorry, your transaction could not be processed...

${JSON.stringify( + errorMessageData, + )}`, + ); }); }); }); } else { // Hides card fields if the merchant isn't eligible - document.querySelector("#card-form").style = "display: none"; + document.querySelector('#card-form').style = 'display: none'; } diff --git a/advanced-integration/views/checkout.ejs b/advanced-integration/views/checkout.ejs index 12522326..85cd7085 100644 --- a/advanced-integration/views/checkout.ejs +++ b/advanced-integration/views/checkout.ejs @@ -1,12 +1,14 @@ - + + - - + + + PayPal JS SDK Advanced Integration + > diff --git a/standard-integration/README.md b/standard-integration/README.md index 829911be..c1c22ad8 100644 --- a/standard-integration/README.md +++ b/standard-integration/README.md @@ -1,12 +1,12 @@ # Standard Integration Example -This folder contains example code for a standard 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 Standard 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) -3. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET` -2. Replace `test` in `index.html` with your app's client-id +2. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET` +3. Replace `test` in `index.html` with your app's client-id 4. Run `npm install` 5. Run `npm start` 6. Open http://localhost:8888 diff --git a/standard-integration/index.html b/standard-integration/index.html index d22c1d1f..2bdc9a4b 100644 --- a/standard-integration/index.html +++ b/standard-integration/index.html @@ -82,12 +82,12 @@ ); } else if (!orderData.purchase_units) { throw new Error(JSON.stringify(orderData)); - } - else { + } 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?.captures?.[0] || + orderData?.purchase_units?.[0]?.payments?.authorizations?.[0]; resultMessage( `Transaction ${transaction.status}: ${transaction.id}

See console for all available details`, ); @@ -107,10 +107,9 @@ }) .render('#paypal-button-container'); - // Example function to show a result to the user. Your site's UI library can be used instead, - // however alert() should not be used as it will interrupt the JS SDK popup window + // Example function to show a result to the user. Your site's UI library can be used instead. function resultMessage(message) { - const container = document.getElementById('result-message'); + const container = document.querySelector('#result-message'); container.innerHTML = message; } diff --git a/standard-integration/package.json b/standard-integration/package.json index 86974e31..e776dad9 100644 --- a/standard-integration/package.json +++ b/standard-integration/package.json @@ -1,5 +1,6 @@ { "name": "paypal-standard-integration", + "description": "Sample Node.js web app to integrate PayPal Standard Checkout for online payments", "version": "1.0.0", "main": "server.js", "type": "module", @@ -7,9 +8,7 @@ "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js" }, - "author": "", "license": "Apache-2.0", - "description": "", "dependencies": { "dotenv": "^16.3.1", "express": "^4.18.2", From dcd953d3b708c4a10e5520997734785092f3b16f Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:04:44 -0500 Subject: [PATCH 15/53] Use double quotes to match the guides (#67) --- advanced-integration/public/app.js | 64 +++++++++++++++--------------- advanced-integration/server.js | 64 +++++++++++++++--------------- standard-integration/index.html | 22 +++++----- standard-integration/server.js | 54 ++++++++++++------------- 4 files changed, 102 insertions(+), 102 deletions(-) diff --git a/advanced-integration/public/app.js b/advanced-integration/public/app.js index b26369c7..02f484f6 100644 --- a/advanced-integration/public/app.js +++ b/advanced-integration/public/app.js @@ -1,17 +1,17 @@ async function createOrderCallback() { try { - const response = await fetch('/api/orders', { - method: 'POST', + const response = await fetch("/api/orders", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "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', + id: "YOUR_PRODUCT_ID", + quantity: "YOUR_PRODUCT_QUANTITY", }, ], }), @@ -38,9 +38,9 @@ async function createOrderCallback() { async function onApproveCallback(data, actions) { try { const response = await fetch(`/api/orders/${data.orderID}/capture`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, }); @@ -55,11 +55,11 @@ async function onApproveCallback(data, actions) { orderData?.purchase_units?.[0]?.payments?.authorizations?.[0]; const errorDetail = orderData?.details?.[0]; - const isHostedFieldsComponent = typeof data.card === 'object'; + const isHostedFieldsComponent = typeof data.card === "object"; // this actions.restart() behavior only applies to the Buttons component if ( - errorDetail?.issue === 'INSTRUMENT_DECLINED' && + errorDetail?.issue === "INSTRUMENT_DECLINED" && isHostedFieldsComponent === false ) { // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() @@ -68,7 +68,7 @@ async function onApproveCallback(data, actions) { } else if ( errorDetail || !transaction || - transaction.status === 'DECLINED' + transaction.status === "DECLINED" ) { // (2) Other non-recoverable errors -> Show a failure message let errorMessage; @@ -88,7 +88,7 @@ async function onApproveCallback(data, actions) { `Transaction ${transaction.status}: ${transaction.id}

See console for all available details`, ); console.log( - 'Capture result', + "Capture result", orderData, JSON.stringify(orderData, null, 2), ); @@ -106,11 +106,11 @@ paypal createOrder: createOrderCallback, onApprove: onApproveCallback, }) - .render('#paypal-button-container'); + .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'); + const container = document.querySelector("#result-message"); container.innerHTML = message; } @@ -121,55 +121,55 @@ if (paypal.HostedFields.isEligible()) { // Call your server to set up the transaction createOrder: createOrderCallback, styles: { - '.valid': { - color: 'green', + ".valid": { + color: "green", }, - '.invalid': { - color: 'red', + ".invalid": { + color: "red", }, }, fields: { number: { - selector: '#card-number', - placeholder: '4111 1111 1111 1111', + selector: "#card-number", + placeholder: "4111 1111 1111 1111", }, cvv: { - selector: '#cvv', - placeholder: '123', + selector: "#cvv", + placeholder: "123", }, expirationDate: { - selector: '#expiration-date', - placeholder: 'MM/YY', + selector: "#expiration-date", + placeholder: "MM/YY", }, }, }).then((cardFields) => { - document.querySelector('#card-form').addEventListener('submit', (event) => { + document.querySelector("#card-form").addEventListener("submit", (event) => { event.preventDefault(); cardFields .submit({ // Cardholder's first and last name - cardholderName: document.getElementById('card-holder-name').value, + cardholderName: document.getElementById("card-holder-name").value, // Billing Address billingAddress: { // Street address, line 1 streetAddress: document.getElementById( - 'card-billing-address-street', + "card-billing-address-street", ).value, // Street address, line 2 (Ex: Unit, Apartment, etc.) extendedAddress: document.getElementById( - 'card-billing-address-unit', + "card-billing-address-unit", ).value, // State - region: document.getElementById('card-billing-address-state').value, + region: document.getElementById("card-billing-address-state").value, // City - locality: document.getElementById('card-billing-address-city') + locality: document.getElementById("card-billing-address-city") .value, // Postal Code - postalCode: document.getElementById('card-billing-address-zip') + postalCode: document.getElementById("card-billing-address-zip") .value, // Country Code countryCodeAlpha2: document.getElementById( - 'card-billing-address-country', + "card-billing-address-country", ).value, }, }) @@ -188,5 +188,5 @@ if (paypal.HostedFields.isEligible()) { }); } else { // Hides card fields if the merchant isn't eligible - document.querySelector('#card-form').style = 'display: none'; + document.querySelector("#card-form").style = "display: none"; } diff --git a/advanced-integration/server.js b/advanced-integration/server.js index fb2d8c35..d8415e00 100644 --- a/advanced-integration/server.js +++ b/advanced-integration/server.js @@ -1,13 +1,13 @@ -import express from 'express'; -import fetch from 'node-fetch'; -import 'dotenv/config'; -import path from 'path'; +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 base = "https://api-m.sandbox.paypal.com"; const app = express(); -app.set('view engine', 'ejs'); -app.use(express.static('public')); +app.set("view engine", "ejs"); +app.use(express.static("public")); // parse post params sent in body in json format app.use(express.json()); @@ -19,14 +19,14 @@ app.use(express.json()); const generateAccessToken = async () => { try { if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) { - throw new Error('MISSING_API_CREDENTIALS'); + throw new Error("MISSING_API_CREDENTIALS"); } const auth = Buffer.from( - PAYPAL_CLIENT_ID + ':' + PAYPAL_CLIENT_SECRET, - ).toString('base64'); + PAYPAL_CLIENT_ID + ":" + PAYPAL_CLIENT_SECRET, + ).toString("base64"); const response = await fetch(`${base}/v1/oauth2/token`, { - method: 'POST', - body: 'grant_type=client_credentials', + method: "POST", + body: "grant_type=client_credentials", headers: { Authorization: `Basic ${auth}`, }, @@ -35,7 +35,7 @@ const generateAccessToken = async () => { const data = await response.json(); return data.access_token; } catch (error) { - console.error('Failed to generate Access Token:', error); + console.error("Failed to generate Access Token:", error); } }; @@ -47,11 +47,11 @@ const generateClientToken = async () => { const accessToken = await generateAccessToken(); const url = `${base}/v1/identity/generate-token`; const response = await fetch(url, { - method: 'POST', + method: "POST", headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Language': 'en_US', - 'Content-Type': 'application/json', + "Accept-Language": "en_US", + "Content-Type": "application/json", }, }); @@ -65,19 +65,19 @@ const generateClientToken = async () => { 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:', + "shopping cart information passed from the frontend createOrder() callback:", cart, ); const accessToken = await generateAccessToken(); const url = `${base}/v2/checkout/orders`; const payload = { - intent: 'CAPTURE', + intent: "CAPTURE", purchase_units: [ { amount: { - currency_code: 'USD', - value: '0.02', + currency_code: "USD", + value: "0.02", }, }, ], @@ -85,7 +85,7 @@ const createOrder = async (cart) => { const response = await fetch(url, { headers: { - 'Content-Type': 'application/json', + "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/ @@ -93,7 +93,7 @@ const createOrder = async (cart) => { // "PayPal-Mock-Response": '{"mock_application_codes": "PERMISSION_DENIED"}' // "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}' }, - method: 'POST', + method: "POST", body: JSON.stringify(payload), }); @@ -109,9 +109,9 @@ const captureOrder = async (orderID) => { const url = `${base}/v2/checkout/orders/${orderID}/capture`; const response = await fetch(url, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "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/ @@ -138,10 +138,10 @@ async function handleResponse(response) { } // render checkout page with client id & unique client token -app.get('/', async (req, res) => { +app.get("/", async (req, res) => { try { const { jsonResponse } = await generateClientToken(); - res.render('checkout', { + res.render("checkout", { clientId: PAYPAL_CLIENT_ID, clientToken: jsonResponse.client_token, }); @@ -150,26 +150,26 @@ app.get('/', async (req, res) => { } }); -app.post('/api/orders', async (req, res) => { +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.' }); + 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) => { +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.' }); + console.error("Failed to create order:", error); + res.status(500).json({ error: "Failed to capture order." }); } }); diff --git a/standard-integration/index.html b/standard-integration/index.html index 2bdc9a4b..6a355374 100644 --- a/standard-integration/index.html +++ b/standard-integration/index.html @@ -15,18 +15,18 @@ .Buttons({ createOrder: async () => { try { - const response = await fetch('/api/orders', { - method: 'POST', + const response = await fetch("/api/orders", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "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', + id: "YOUR_PRODUCT_ID", + quantity: "YOUR_PRODUCT_QUANTITY", }, ], }), @@ -56,9 +56,9 @@ const response = await fetch( `/api/orders/${data.orderID}/capture`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, }, ); @@ -71,7 +71,7 @@ const errorDetail = orderData?.details?.[0]; - if (errorDetail?.issue === 'INSTRUMENT_DECLINED') { + 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(); @@ -92,7 +92,7 @@ `Transaction ${transaction.status}: ${transaction.id}

See console for all available details`, ); console.log( - 'Capture result', + "Capture result", orderData, JSON.stringify(orderData, null, 2), ); @@ -105,11 +105,11 @@ } }, }) - .render('#paypal-button-container'); + .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'); + const container = document.querySelector("#result-message"); container.innerHTML = message; } diff --git a/standard-integration/server.js b/standard-integration/server.js index 7b7310f0..76f99226 100644 --- a/standard-integration/server.js +++ b/standard-integration/server.js @@ -1,10 +1,10 @@ -import express from 'express'; -import fetch from 'node-fetch'; -import 'dotenv/config'; -import path from 'path'; +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 base = "https://api-m.sandbox.paypal.com"; const app = express(); // parse post params sent in body in json format @@ -17,14 +17,14 @@ app.use(express.json()); const generateAccessToken = async () => { try { if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) { - throw new Error('MISSING_API_CREDENTIALS'); + throw new Error("MISSING_API_CREDENTIALS"); } const auth = Buffer.from( - PAYPAL_CLIENT_ID + ':' + PAYPAL_CLIENT_SECRET, - ).toString('base64'); + PAYPAL_CLIENT_ID + ":" + PAYPAL_CLIENT_SECRET, + ).toString("base64"); const response = await fetch(`${base}/v1/oauth2/token`, { - method: 'POST', - body: 'grant_type=client_credentials', + method: "POST", + body: "grant_type=client_credentials", headers: { Authorization: `Basic ${auth}`, }, @@ -33,7 +33,7 @@ const generateAccessToken = async () => { const data = await response.json(); return data.access_token; } catch (error) { - console.error('Failed to generate Access Token:', error); + console.error("Failed to generate Access Token:", error); } }; @@ -44,19 +44,19 @@ const generateAccessToken = async () => { 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:', + "shopping cart information passed from the frontend createOrder() callback:", cart, ); const accessToken = await generateAccessToken(); const url = `${base}/v2/checkout/orders`; const payload = { - intent: 'CAPTURE', + intent: "CAPTURE", purchase_units: [ { amount: { - currency_code: 'USD', - value: '0.02', + currency_code: "USD", + value: "0.02", }, }, ], @@ -64,7 +64,7 @@ const createOrder = async (cart) => { const response = await fetch(url, { headers: { - 'Content-Type': 'application/json', + "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/ @@ -72,7 +72,7 @@ const createOrder = async (cart) => { // "PayPal-Mock-Response": '{"mock_application_codes": "PERMISSION_DENIED"}' // "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}' }, - method: 'POST', + method: "POST", body: JSON.stringify(payload), }); @@ -88,9 +88,9 @@ const captureOrder = async (orderID) => { const url = `${base}/v2/checkout/orders/${orderID}/capture`; const response = await fetch(url, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "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/ @@ -116,32 +116,32 @@ async function handleResponse(response) { } } -app.post('/api/orders', async (req, res) => { +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.' }); + 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) => { +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.' }); + 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('./index.html')); +app.get("/", (req, res) => { + res.sendFile(path.resolve("./index.html")); }); app.listen(PORT, () => { From 442e524df75398f71ed3078ad3553037a95c5f83 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:30:30 -0500 Subject: [PATCH 16/53] Improve the check for instrument declined use case (#68) --- advanced-integration/public/app.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/advanced-integration/public/app.js b/advanced-integration/public/app.js index 02f484f6..43fb25d9 100644 --- a/advanced-integration/public/app.js +++ b/advanced-integration/public/app.js @@ -55,13 +55,8 @@ async function onApproveCallback(data, actions) { orderData?.purchase_units?.[0]?.payments?.authorizations?.[0]; const errorDetail = orderData?.details?.[0]; - const isHostedFieldsComponent = typeof data.card === "object"; - // this actions.restart() behavior only applies to the Buttons component - if ( - errorDetail?.issue === "INSTRUMENT_DECLINED" && - isHostedFieldsComponent === false - ) { + 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(); From 1026de28d9e92ab9625ea86f6e543928a7758dc4 Mon Sep 17 00:00:00 2001 From: Arit Developer <32520970+msarit@users.noreply.github.com> Date: Fri, 18 Aug 2023 11:10:13 -0400 Subject: [PATCH 17/53] update order amount values (#69) --- advanced-integration/server.js | 2 +- standard-integration/server.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/advanced-integration/server.js b/advanced-integration/server.js index d8415e00..becdb6a2 100644 --- a/advanced-integration/server.js +++ b/advanced-integration/server.js @@ -77,7 +77,7 @@ const createOrder = async (cart) => { { amount: { currency_code: "USD", - value: "0.02", + value: "100.00", }, }, ], diff --git a/standard-integration/server.js b/standard-integration/server.js index 76f99226..1e998011 100644 --- a/standard-integration/server.js +++ b/standard-integration/server.js @@ -56,7 +56,7 @@ const createOrder = async (cart) => { { amount: { currency_code: "USD", - value: "0.02", + value: "100.00", }, }, ], From fe0d6899f31bd3aeaa012773d38f2ca5bc2e0129 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Fri, 18 Aug 2023 15:20:00 -0500 Subject: [PATCH 18/53] Use a new client/server folder structure (#70) --- .../{public => client}/app.js | 0 advanced-integration/package.json | 5 +- advanced-integration/{ => server}/server.js | 4 +- .../{ => server}/views/checkout.ejs | 0 standard-integration/README.md | 2 +- standard-integration/client/app.js | 94 ++++++++++++++ standard-integration/client/checkout.html | 15 +++ standard-integration/index.html | 117 ------------------ standard-integration/package.json | 5 +- standard-integration/{ => server}/server.js | 5 +- 10 files changed, 122 insertions(+), 125 deletions(-) rename advanced-integration/{public => client}/app.js (100%) rename advanced-integration/{ => server}/server.js (98%) rename advanced-integration/{ => server}/views/checkout.ejs (100%) create mode 100644 standard-integration/client/app.js create mode 100644 standard-integration/client/checkout.html delete mode 100644 standard-integration/index.html rename standard-integration/{ => server}/server.js (97%) diff --git a/advanced-integration/public/app.js b/advanced-integration/client/app.js similarity index 100% rename from advanced-integration/public/app.js rename to advanced-integration/client/app.js diff --git a/advanced-integration/package.json b/advanced-integration/package.json index 424f853e..1d8d34ed 100644 --- a/advanced-integration/package.json +++ b/advanced-integration/package.json @@ -2,11 +2,12 @@ "name": "paypal-advanced-integration", "description": "Sample Node.js web app to integrate PayPal Advanced Checkout for online payments", "version": "1.0.0", - "main": "server.js", + "main": "server/server.js", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node server.js" + "start": "node server/server.js", + "format": "npx prettier --write **.js" }, "license": "Apache-2.0", "dependencies": { diff --git a/advanced-integration/server.js b/advanced-integration/server/server.js similarity index 98% rename from advanced-integration/server.js rename to advanced-integration/server/server.js index becdb6a2..a7d84407 100644 --- a/advanced-integration/server.js +++ b/advanced-integration/server/server.js @@ -1,13 +1,13 @@ 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(); app.set("view engine", "ejs"); -app.use(express.static("public")); +app.set("views", "./server/views"); +app.use(express.static("client")); // parse post params sent in body in json format app.use(express.json()); diff --git a/advanced-integration/views/checkout.ejs b/advanced-integration/server/views/checkout.ejs similarity index 100% rename from advanced-integration/views/checkout.ejs rename to advanced-integration/server/views/checkout.ejs diff --git a/standard-integration/README.md b/standard-integration/README.md index c1c22ad8..c0bf83f3 100644 --- a/standard-integration/README.md +++ b/standard-integration/README.md @@ -6,7 +6,7 @@ This folder contains example code for a Standard PayPal integration using both t 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 `index.html` with your app's client-id +3. Replace `test` in `client/index.html` with your app's client-id 4. Run `npm install` 5. Run `npm start` 6. Open http://localhost:8888 diff --git a/standard-integration/client/app.js b/standard-integration/client/app.js new file mode 100644 index 00000000..af056c7d --- /dev/null +++ b/standard-integration/client/app.js @@ -0,0 +1,94 @@ +window.paypal + .Buttons({ + createOrder: async () => { + 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}`); + } + }, + onApprove: async (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`, + ); + 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/standard-integration/client/checkout.html b/standard-integration/client/checkout.html new file mode 100644 index 00000000..7b959c2f --- /dev/null +++ b/standard-integration/client/checkout.html @@ -0,0 +1,15 @@ + + + + + + PayPal JS SDK Standard Integration + + +
+

+ + + + + diff --git a/standard-integration/index.html b/standard-integration/index.html deleted file mode 100644 index 6a355374..00000000 --- a/standard-integration/index.html +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - PayPal JS SDK Standard Integration - - -
-

- - - - - diff --git a/standard-integration/package.json b/standard-integration/package.json index e776dad9..3c26d897 100644 --- a/standard-integration/package.json +++ b/standard-integration/package.json @@ -2,11 +2,12 @@ "name": "paypal-standard-integration", "description": "Sample Node.js web app to integrate PayPal Standard Checkout for online payments", "version": "1.0.0", - "main": "server.js", + "main": "server/server.js", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node server.js" + "start": "node server/server.js", + "format": "npx prettier --write **.js" }, "license": "Apache-2.0", "dependencies": { diff --git a/standard-integration/server.js b/standard-integration/server/server.js similarity index 97% rename from standard-integration/server.js rename to standard-integration/server/server.js index 1e998011..0d8d3cb8 100644 --- a/standard-integration/server.js +++ b/standard-integration/server/server.js @@ -7,6 +7,9 @@ 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()); @@ -141,7 +144,7 @@ app.post("/api/orders/:orderID/capture", async (req, res) => { // serve index.html app.get("/", (req, res) => { - res.sendFile(path.resolve("./index.html")); + res.sendFile(path.resolve("./client/checkout.html")); }); app.listen(PORT, () => { From 43d675a39df5166d184178b163ddcb7e32ed2db4 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Mon, 21 Aug 2023 10:37:10 -0500 Subject: [PATCH 19/53] Use GitHub Actions to enforce code formatting and lint rules (#72) --- .eslintrc.json | 11 +++++++++++ .github/workflows/validate.yml | 27 +++++++++++++++++++++++++++ advanced-integration/package.json | 4 +++- standard-integration/package.json | 4 +++- 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 .eslintrc.json create mode 100644 .github/workflows/validate.yml diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..ed606c67 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "env": { + "es2021": true + }, + "extends": ["eslint:recommended"], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": {} +} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 00000000..ca6da048 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,27 @@ +name: validate +on: + # run on push but only for the main branch + push: + branches: + - main + # run for every pull request + pull_request: {} +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: ⎔ Setup node + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: 🧹 Check code formatting with Prettier + run: find . -name package.json -maxdepth 2 -type f -execdir npm run format:check ';' + + - name: 👕 Lint Node.js code with ESLint + run: find . -name package.json -maxdepth 2 -type f -execdir npm run lint ';' diff --git a/advanced-integration/package.json b/advanced-integration/package.json index 1d8d34ed..8616dad9 100644 --- a/advanced-integration/package.json +++ b/advanced-integration/package.json @@ -7,7 +7,9 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node server/server.js", - "format": "npx prettier --write **.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": { diff --git a/standard-integration/package.json b/standard-integration/package.json index 3c26d897..8c51888d 100644 --- a/standard-integration/package.json +++ b/standard-integration/package.json @@ -7,7 +7,9 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node server/server.js", - "format": "npx prettier --write **.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": { From 9c9ed82698262ac2fcb0315daebd76cd1a7d4236 Mon Sep 17 00:00:00 2001 From: KalpanaReddyC <33893559+KalpanaReddyC@users.noreply.github.com> Date: Mon, 21 Aug 2023 21:54:48 +0530 Subject: [PATCH 20/53] Improve config for codespaces (#71) --- .devcontainer/advanced-integration/devcontainer.json | 10 ++-------- .devcontainer/advanced-integration/welcome-message.sh | 4 ++-- .devcontainer/standard-integration/devcontainer.json | 10 ++-------- .devcontainer/standard-integration/welcome-message.sh | 4 ++-- README.md | 9 ++++++++- 5 files changed, 16 insertions(+), 21 deletions(-) diff --git a/.devcontainer/advanced-integration/devcontainer.json b/.devcontainer/advanced-integration/devcontainer.json index 9b4e4d0d..7eb20bdd 100644 --- a/.devcontainer/advanced-integration/devcontainer.json +++ b/.devcontainer/advanced-integration/devcontainer.json @@ -1,20 +1,16 @@ // For more details, see https://aka.ms/devcontainer.json. { "name": "PayPal Advanced Integration", - "image": "mcr.microsoft.com/devcontainers/universal:2", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration", - // Use 'onCreateCommand' to run commands when creating the container. "onCreateCommand": "bash ../.devcontainer/advanced-integration/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 @@ -25,7 +21,6 @@ "onAutoForward": "openBrowserOnce" } }, - "secrets": { "PAYPAL_CLIENT_ID": { "description": "Sandbox client ID of the application.", @@ -36,7 +31,6 @@ "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" } }, - "customizations": { "vscode": { "extensions": [ @@ -47,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/.devcontainer/advanced-integration/welcome-message.sh b/.devcontainer/advanced-integration/welcome-message.sh index a37ec162..ae9a72f9 100644 --- a/.devcontainer/advanced-integration/welcome-message.sh +++ b/.devcontainer/advanced-integration/welcome-message.sh @@ -7,7 +7,7 @@ WELCOME_MESSAGE=" 🛠️ Your environment is fully setup with all the required software. -🚀 Once you rename the \".env.example\" file to \".env\" and update \"CLIENT_ID\" and \"APP_SECRET\", the checkout page will automatically open in the browser after the server is restarted." +🚀 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\" @@ -16,7 +16,7 @@ ALTERNATE_WELCOME_MESSAGE=" 🚀 The checkout page will automatically open in the browser after the server is started." -if [ -n "$CLIENT_ID" ] && [ -n "$APP_SECRET" ]; then +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" fi diff --git a/.devcontainer/standard-integration/devcontainer.json b/.devcontainer/standard-integration/devcontainer.json index 22576145..742d6534 100644 --- a/.devcontainer/standard-integration/devcontainer.json +++ b/.devcontainer/standard-integration/devcontainer.json @@ -1,20 +1,16 @@ // For more details, see https://aka.ms/devcontainer.json. { "name": "PayPal Standard Integration", - "image": "mcr.microsoft.com/devcontainers/universal:2", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/standard-integration", - // Use 'onCreateCommand' to run commands when creating the container. "onCreateCommand": "bash ../.devcontainer/standard-integration/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 @@ -25,7 +21,6 @@ "onAutoForward": "openBrowserOnce" } }, - "secrets": { "PAYPAL_CLIENT_ID": { "description": "Sandbox client ID of the application.", @@ -36,7 +31,6 @@ "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" } }, - "customizations": { "vscode": { "extensions": [ @@ -47,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/.devcontainer/standard-integration/welcome-message.sh b/.devcontainer/standard-integration/welcome-message.sh index debc7864..78cce216 100644 --- a/.devcontainer/standard-integration/welcome-message.sh +++ b/.devcontainer/standard-integration/welcome-message.sh @@ -7,7 +7,7 @@ WELCOME_MESSAGE=" 🛠️ Your environment is fully setup with all the required software. -🚀 Once you rename the \".env.example\" file to \".env\" and update \"CLIENT_ID\" and \"APP_SECRET\", the checkout page will automatically open in the browser after the server is restarted." +🚀 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 Standard Checkout Integration Example\" @@ -16,7 +16,7 @@ ALTERNATE_WELCOME_MESSAGE=" 🚀 The checkout page will automatically open in the browser after the server is started." -if [ -n "$CLIENT_ID" ] && [ -n "$APP_SECRET" ]; then +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" fi diff --git a/README.md b/README.md index d37a5e98..e2f4e583 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,11 @@ Once you've setup a PayPal account, you'll need to obtain a **Client ID** and ** These examples will ask you to run commands like `npm install` and `npm start`. -You'll need a version of node >= 16 which can be downloaded from the [Node.js website](https://nodejs.org/en/download/). \ No newline at end of file +You'll need a version of node >= 16 which can be downloaded from the [Node.js website](https://nodejs.org/en/download/). + + +### PayPal Codespaces Links +| Application | Codespaces Link | +| ---- | ---- | +| Advanced Integration | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fadvanced-integration%2Fdevcontainer.json)| +| Standard Integration | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fstandard-integration%2Fdevcontainer.json)| \ No newline at end of file From bb8bab464b6ab027c2d9aba21acd6cd6edba3356 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:36:32 -0500 Subject: [PATCH 21/53] Respect exit codes in CI and format json (#73) --- .../advanced-integration/devcontainer.json | 80 +++++++++---------- .../standard-integration/devcontainer.json | 80 +++++++++---------- .github/workflows/validate.yml | 14 +++- advanced-integration/client/app.js | 9 +-- 4 files changed, 91 insertions(+), 92 deletions(-) diff --git a/.devcontainer/advanced-integration/devcontainer.json b/.devcontainer/advanced-integration/devcontainer.json index 7eb20bdd..2e840f6f 100644 --- a/.devcontainer/advanced-integration/devcontainer.json +++ b/.devcontainer/advanced-integration/devcontainer.json @@ -1,44 +1,40 @@ // For more details, see https://aka.ms/devcontainer.json. { - "name": "PayPal Advanced Integration", - "image": "mcr.microsoft.com/devcontainers/javascript-node:20", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration", - // Use 'onCreateCommand' to run commands when creating the container. - "onCreateCommand": "bash ../.devcontainer/advanced-integration/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" - } - } - } -} \ No newline at end of file + "name": "PayPal Advanced Integration", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash ../.devcontainer/advanced-integration/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/standard-integration/devcontainer.json b/.devcontainer/standard-integration/devcontainer.json index 742d6534..846dd982 100644 --- a/.devcontainer/standard-integration/devcontainer.json +++ b/.devcontainer/standard-integration/devcontainer.json @@ -1,44 +1,40 @@ // For more details, see https://aka.ms/devcontainer.json. { - "name": "PayPal Standard Integration", - "image": "mcr.microsoft.com/devcontainers/javascript-node:20", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/standard-integration", - // Use 'onCreateCommand' to run commands when creating the container. - "onCreateCommand": "bash ../.devcontainer/standard-integration/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 Standard 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" - } - } - } -} \ No newline at end of file + "name": "PayPal Standard Integration", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/standard-integration", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash ../.devcontainer/standard-integration/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 Standard 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/.github/workflows/validate.yml b/.github/workflows/validate.yml index ca6da048..ba842418 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -21,7 +21,15 @@ jobs: node-version: 18 - name: 🧹 Check code formatting with Prettier - run: find . -name package.json -maxdepth 2 -type f -execdir npm run format:check ';' + run: > + find . -name package.json -maxdepth 2 -type f | while read -r file; do + directory=$(dirname "$file") + cd "$directory" && npm run format:check && cd - + done - - name: 👕 Lint Node.js code with ESLint - run: find . -name package.json -maxdepth 2 -type f -execdir npm run lint ';' + - name: 👕 Lint code with ESLint + run: > + find . -name package.json -maxdepth 2 -type f | while read -r file; do + directory=$(dirname "$file") + cd "$directory" && npm run lint && cd - + done diff --git a/advanced-integration/client/app.js b/advanced-integration/client/app.js index 43fb25d9..65f048d7 100644 --- a/advanced-integration/client/app.js +++ b/advanced-integration/client/app.js @@ -96,7 +96,7 @@ async function onApproveCallback(data, actions) { } } -paypal +window.paypal .Buttons({ createOrder: createOrderCallback, onApprove: onApproveCallback, @@ -110,9 +110,9 @@ function resultMessage(message) { } // If this returns false or the card fields aren't visible, see Step #1. -if (paypal.HostedFields.isEligible()) { +if (window.paypal.HostedFields.isEligible()) { // Renders card fields - paypal.HostedFields.render({ + window.paypal.HostedFields.render({ // Call your server to set up the transaction createOrder: createOrderCallback, styles: { @@ -172,10 +172,9 @@ if (paypal.HostedFields.isEligible()) { return onApproveCallback(data); }) .catch((orderData) => { - const { links, ...errorMessageData } = orderData; resultMessage( `Sorry, your transaction could not be processed...

${JSON.stringify( - errorMessageData, + orderData, )}`, ); }); From 1fedc8d79f9f7ca907065ff4d52603b1ce0fa06d Mon Sep 17 00:00:00 2001 From: Nikema <3941856+prophen@users.noreply.github.com> Date: Mon, 21 Aug 2023 14:35:15 -0700 Subject: [PATCH 22/53] Add nodemon for restarting server on changes (#74) --- advanced-integration/package.json | 5 ++++- standard-integration/package.json | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/advanced-integration/package.json b/advanced-integration/package.json index 8616dad9..c7211908 100644 --- a/advanced-integration/package.json +++ b/advanced-integration/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node server/server.js", + "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" @@ -17,5 +17,8 @@ "ejs": "^3.1.9", "express": "^4.18.2", "node-fetch": "^3.3.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" } } diff --git a/standard-integration/package.json b/standard-integration/package.json index 8c51888d..310fefad 100644 --- a/standard-integration/package.json +++ b/standard-integration/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node server/server.js", + "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" @@ -16,5 +16,8 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "node-fetch": "^3.3.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" } } From 5788f934ae0780675bee130b5d7a6b544bd4dd3a Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Mon, 21 Aug 2023 16:47:49 -0500 Subject: [PATCH 23/53] Stop quoting values in example env files (#75) --- advanced-integration/.env.example | 4 ++-- standard-integration/.env.example | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/advanced-integration/.env.example b/advanced-integration/.env.example index 0fb8a60a..2251fbbb 100644 --- a/advanced-integration/.env.example +++ b/advanced-integration/.env.example @@ -1,5 +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" +PAYPAL_CLIENT_ID=YOUR_CLIENT_ID_GOES_HERE +PAYPAL_CLIENT_SECRET=YOUR_SECRET_GOES_HERE diff --git a/standard-integration/.env.example b/standard-integration/.env.example index 0fb8a60a..2251fbbb 100644 --- a/standard-integration/.env.example +++ b/standard-integration/.env.example @@ -1,5 +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" +PAYPAL_CLIENT_ID=YOUR_CLIENT_ID_GOES_HERE +PAYPAL_CLIENT_SECRET=YOUR_SECRET_GOES_HERE From 1d0216d5d4d9a97e9bfbd6f9484316b42997e6fc Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Tue, 22 Aug 2023 13:07:46 -0500 Subject: [PATCH 24/53] Use method definition syntax and fix glob pattern for prettier (#76) --- advanced-integration/package.json | 4 ++-- standard-integration/client/app.js | 4 ++-- standard-integration/package.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/advanced-integration/package.json b/advanced-integration/package.json index c7211908..ff3f5b41 100644 --- a/advanced-integration/package.json +++ b/advanced-integration/package.json @@ -7,8 +7,8 @@ "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}", + "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", diff --git a/standard-integration/client/app.js b/standard-integration/client/app.js index af056c7d..cab942a0 100644 --- a/standard-integration/client/app.js +++ b/standard-integration/client/app.js @@ -1,6 +1,6 @@ window.paypal .Buttons({ - createOrder: async () => { + async createOrder() { try { const response = await fetch("/api/orders", { method: "POST", @@ -36,7 +36,7 @@ window.paypal resultMessage(`Could not initiate PayPal Checkout...

${error}`); } }, - onApprove: async (data, actions) => { + async onApprove(data, actions) { try { const response = await fetch(`/api/orders/${data.orderID}/capture`, { method: "POST", diff --git a/standard-integration/package.json b/standard-integration/package.json index 310fefad..5413d87e 100644 --- a/standard-integration/package.json +++ b/standard-integration/package.json @@ -7,8 +7,8 @@ "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}", + "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", From cb2f6a97066391b5930166897c3f4841b633757e Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Thu, 31 Aug 2023 12:51:53 -0500 Subject: [PATCH 25/53] fix: temporarily revert back to old code examples for advanced (#78) --- advanced-integration/new/README.md | 11 ++ advanced-integration/{ => new}/client/app.js | 0 advanced-integration/new/package.json | 24 +++ .../{ => new}/server/server.js | 0 .../{ => new}/server/views/checkout.ejs | 0 advanced-integration/package.json | 4 +- advanced-integration/paypal-api.js | 100 +++++++++++ advanced-integration/public/app.js | 160 ++++++++++++++++++ advanced-integration/server.js | 44 +++++ advanced-integration/views/checkout.ejs | 101 +++++++++++ 10 files changed, 442 insertions(+), 2 deletions(-) create mode 100644 advanced-integration/new/README.md rename advanced-integration/{ => new}/client/app.js (100%) create mode 100644 advanced-integration/new/package.json rename advanced-integration/{ => new}/server/server.js (100%) rename advanced-integration/{ => new}/server/views/checkout.ejs (100%) create mode 100644 advanced-integration/paypal-api.js create mode 100644 advanced-integration/public/app.js create mode 100644 advanced-integration/server.js create mode 100644 advanced-integration/views/checkout.ejs diff --git a/advanced-integration/new/README.md b/advanced-integration/new/README.md new file mode 100644 index 00000000..923a5234 --- /dev/null +++ b/advanced-integration/new/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/client/app.js b/advanced-integration/new/client/app.js similarity index 100% rename from advanced-integration/client/app.js rename to advanced-integration/new/client/app.js diff --git a/advanced-integration/new/package.json b/advanced-integration/new/package.json new file mode 100644 index 00000000..ff3f5b41 --- /dev/null +++ b/advanced-integration/new/package.json @@ -0,0 +1,24 @@ +{ + "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", + "ejs": "^3.1.9", + "express": "^4.18.2", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/advanced-integration/server/server.js b/advanced-integration/new/server/server.js similarity index 100% rename from advanced-integration/server/server.js rename to advanced-integration/new/server/server.js diff --git a/advanced-integration/server/views/checkout.ejs b/advanced-integration/new/server/views/checkout.ejs similarity index 100% rename from advanced-integration/server/views/checkout.ejs rename to advanced-integration/new/server/views/checkout.ejs diff --git a/advanced-integration/package.json b/advanced-integration/package.json index ff3f5b41..d17aa000 100644 --- a/advanced-integration/package.json +++ b/advanced-integration/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/paypal-api.js new file mode 100644 index 00000000..6e6c8aaf --- /dev/null +++ b/advanced-integration/paypal-api.js @@ -0,0 +1,100 @@ +import fetch from "node-fetch"; + +// set some important variables +const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET } = process.env; +const base = "https://api-m.sandbox.paypal.com"; + +/** + * Create an order + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create + */ +export async function createOrder() { + const purchaseAmount = "100.00"; // TODO: pull prices from a database + const accessToken = await generateAccessToken(); + const url = `${base}/v2/checkout/orders`; + const response = await fetch(url, { + method: "post", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + intent: "CAPTURE", + purchase_units: [ + { + amount: { + currency_code: "USD", + value: purchaseAmount, + }, + }, + ], + }), + }); + + return handleResponse(response); +} + +/** + * Capture payment for an order + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture + */ +export async function capturePayment(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}`, + }, + }); + + return handleResponse(response); +} + +/** + * Generate an OAuth 2.0 access token + * @see https://developer.paypal.com/api/rest/authentication/ + */ +export async function generateAccessToken() { + 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 jsonData = await handleResponse(response); + return jsonData.access_token; +} + +/** + * Generate a client token + * @see https://developer.paypal.com/docs/checkout/advanced/integrate/#link-sampleclienttokenrequest + */ +export async function generateClientToken() { + const accessToken = await generateAccessToken(); + const response = await fetch(`${base}/v1/identity/generate-token`, { + method: "post", + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Language": "en_US", + "Content-Type": "application/json", + }, + }); + console.log("response", response.status); + const jsonData = await handleResponse(response); + return jsonData.client_token; +} + +async function handleResponse(response) { + if (response.status === 200 || response.status === 201) { + return response.json(); + } + + const errorMessage = await response.text(); + throw new Error(errorMessage); +} diff --git a/advanced-integration/public/app.js b/advanced-integration/public/app.js new file mode 100644 index 00000000..f475472d --- /dev/null +++ b/advanced-integration/public/app.js @@ -0,0 +1,160 @@ +window.paypal + .Buttons({ + // Sets up the transaction when a payment button is clicked + createOrder: function () { + return fetch("/api/orders", { + method: "post", + // use the "body" param to optionally pass additional order information + // like product skus and quantities + body: JSON.stringify({ + cart: [ + { + sku: "", + quantity: "", + }, + ], + }), + }) + .then((response) => response.json()) + .then((order) => order.id); + }, + // Finalize the transaction after payer approval + onApprove: function (data) { + return fetch(`/api/orders/${data.orderID}/capture`, { + method: "post", + }) + .then((response) => response.json()) + .then((orderData) => { + // Successful capture! For dev/demo purposes: + console.log( + "Capture result", + orderData, + JSON.stringify(orderData, null, 2), + ); + const transaction = orderData.purchase_units[0].payments.captures[0]; + alert(`Transaction ${transaction.status}: ${transaction.id} + + See console for all available details + `); + // When ready to go live, remove the alert and show a success message within this page. For example: + // var element = document.getElementById('paypal-button-container'); + // element.innerHTML = '

Thank you for your payment!

'; + // Or go to another URL: actions.redirect('thank_you.html'); + }); + }, + }) + .render("#paypal-button-container"); + +// If this returns false or the card fields aren't visible, see Step #1. +if (window.paypal.HostedFields.isEligible()) { + let orderId; + + // Renders card fields + window.paypal.HostedFields.render({ + // Call your server to set up the transaction + createOrder: () => { + return fetch("/api/orders", { + method: "post", + // use the "body" param to optionally pass additional order information + // like product skus and quantities + body: JSON.stringify({ + cart: [ + { + sku: "", + quantity: "", + }, + ], + }), + }) + .then((res) => res.json()) + .then((orderData) => { + orderId = orderData.id; // needed later to complete capture + return orderData.id; + }); + }, + 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(() => { + fetch(`/api/orders/${orderId}/capture`, { + method: "post", + }) + .then((res) => res.json()) + .then((orderData) => { + // Two cases to handle: + // (1) Other non-recoverable errors -> Show a failure message + // (2) Successful transaction -> Show confirmation or thank you + // This example reads a v2/checkout/orders capture response, propagated from the server + // You could use a different API or structure for your 'orderData' + const errorDetail = + Array.isArray(orderData.details) && orderData.details[0]; + if (errorDetail) { + var msg = "Sorry, your transaction could not be processed."; + if (errorDetail.description) + msg += "\n\n" + errorDetail.description; + if (orderData.debug_id) msg += " (" + orderData.debug_id + ")"; + return alert(msg); // Show a failure message + } + // Show a success message or redirect + alert("Transaction completed!"); + }); + }) + .catch((err) => { + alert("Payment could not be captured! " + JSON.stringify(err)); + }); + }); + }); +} else { + // Hides card fields if the merchant isn't eligible + document.querySelector("#card-form").style = "display: none"; +} diff --git a/advanced-integration/server.js b/advanced-integration/server.js new file mode 100644 index 00000000..73076fcb --- /dev/null +++ b/advanced-integration/server.js @@ -0,0 +1,44 @@ +import "dotenv/config"; +import express from "express"; +import * as paypal from "./paypal-api.js"; +const { PORT = 8888 } = process.env; + +const app = express(); +app.set("view engine", "ejs"); +app.use(express.static("public")); + +// render checkout page with client id & unique client token +app.get("/", async (req, res) => { + const clientId = process.env.PAYPAL_CLIENT_ID; + try { + const clientToken = await paypal.generateClientToken(); + res.render("checkout", { clientId, clientToken }); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// create order +app.post("/api/orders", async (req, res) => { + try { + const order = await paypal.createOrder(); + res.json(order); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// capture payment +app.post("/api/orders/:orderID/capture", async (req, res) => { + const { orderID } = req.params; + try { + const captureData = await paypal.capturePayment(orderID); + res.json(captureData); + } catch (err) { + res.status(500).send(err.message); + } +}); + +app.listen(PORT, () => { + console.log(`Server listening at http://localhost:${PORT}/`); +}); diff --git a/advanced-integration/views/checkout.ejs b/advanced-integration/views/checkout.ejs new file mode 100644 index 00000000..12522326 --- /dev/null +++ b/advanced-integration/views/checkout.ejs @@ -0,0 +1,101 @@ + + + + + + + + +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+

+ +
+
+ + + From fa12c9c095bf95bd07be85f0cfd64aa71ea9240f Mon Sep 17 00:00:00 2001 From: sdarshale <141275730+sdarshale@users.noreply.github.com> Date: Mon, 18 Sep 2023 15:25:37 -0500 Subject: [PATCH 26/53] Update README with codespace details (#80) --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e2f4e583..34a45ef6 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,22 @@ These examples will ask you to run commands like `npm install` and `npm start`. You'll need a version of node >= 16 which can be downloaded from the [Node.js website](https://nodejs.org/en/download/). -### PayPal Codespaces Links +## PayPal Codespaces + +PayPal codespaces require a client ID and client secret for your app. + +### Link to codespaces + | Application | Codespaces Link | | ---- | ---- | | Advanced Integration | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fadvanced-integration%2Fdevcontainer.json)| -| Standard Integration | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fstandard-integration%2Fdevcontainer.json)| \ No newline at end of file +| Standard Integration | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fstandard-integration%2Fdevcontainer.json)| + +### Learn more + +You can read more about codespaces in the [PayPal Developer Docs](https://developer.paypal.com/api/rest/sandbox/codespaces). + +## Submit Feedback + +* To report a bug or suggest a new feature, create an [issue in GitHub](https://github.com/paypal-examples/paypaldevsupport/issues/new/choose). +* To submit feedback, go to [GitHub Codespaces](https://developer.paypal.com/api/rest/sandbox/codespaces) and select the "Feedback" tab. From e26deb2111246895d7bd3184cc0b77261cdc6f45 Mon Sep 17 00:00:00 2001 From: sdarshale <141275730+sdarshale@users.noreply.github.com> Date: Fri, 22 Sep 2023 16:07:21 -0500 Subject: [PATCH 27/53] chore(docs): update readme feedback header section (#81) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34a45ef6..06ce3837 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ PayPal codespaces require a client ID and client secret for your app. You can read more about codespaces in the [PayPal Developer Docs](https://developer.paypal.com/api/rest/sandbox/codespaces). -## Submit Feedback +### Feedback * To report a bug or suggest a new feature, create an [issue in GitHub](https://github.com/paypal-examples/paypaldevsupport/issues/new/choose). * To submit feedback, go to [GitHub Codespaces](https://developer.paypal.com/api/rest/sandbox/codespaces) and select the "Feedback" tab. From 53f4e726e51fabb4595f40c78aa1421e537ebd40 Mon Sep 17 00:00:00 2001 From: cnallam <130782580+cnallam@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:42:07 -0700 Subject: [PATCH 28/53] Update README.md for typo fix (#83) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06ce3837..b0bba29e 100644 --- a/README.md +++ b/README.md @@ -59,4 +59,4 @@ You can read more about codespaces in the [PayPal Developer Docs](https://develo ### Feedback * To report a bug or suggest a new feature, create an [issue in GitHub](https://github.com/paypal-examples/paypaldevsupport/issues/new/choose). -* To submit feedback, go to [GitHub Codespaces](https://developer.paypal.com/api/rest/sandbox/codespaces) and select the "Feedback" tab. +* To submit feedback, go to [PayPal Developer Docs](https://developer.paypal.com/api/rest/sandbox/codespaces) and select the "Feedback" tab. From d3c1ada47587a24c9b96e3f02d4617ca5889b1e5 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:08:51 -0500 Subject: [PATCH 29/53] Rename the advanced integration "new" folder to "v1" (#85) --- advanced-integration/{new => v1}/README.md | 0 advanced-integration/{new => v1}/client/app.js | 0 advanced-integration/{new => v1}/package.json | 0 advanced-integration/{new => v1}/server/server.js | 0 advanced-integration/{new => v1}/server/views/checkout.ejs | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename advanced-integration/{new => v1}/README.md (100%) rename advanced-integration/{new => v1}/client/app.js (100%) rename advanced-integration/{new => v1}/package.json (100%) rename advanced-integration/{new => v1}/server/server.js (100%) rename advanced-integration/{new => v1}/server/views/checkout.ejs (100%) diff --git a/advanced-integration/new/README.md b/advanced-integration/v1/README.md similarity index 100% rename from advanced-integration/new/README.md rename to advanced-integration/v1/README.md diff --git a/advanced-integration/new/client/app.js b/advanced-integration/v1/client/app.js similarity index 100% rename from advanced-integration/new/client/app.js rename to advanced-integration/v1/client/app.js diff --git a/advanced-integration/new/package.json b/advanced-integration/v1/package.json similarity index 100% rename from advanced-integration/new/package.json rename to advanced-integration/v1/package.json diff --git a/advanced-integration/new/server/server.js b/advanced-integration/v1/server/server.js similarity index 100% rename from advanced-integration/new/server/server.js rename to advanced-integration/v1/server/server.js diff --git a/advanced-integration/new/server/views/checkout.ejs b/advanced-integration/v1/server/views/checkout.ejs similarity index 100% rename from advanced-integration/new/server/views/checkout.ejs rename to advanced-integration/v1/server/views/checkout.ejs From 5c89c3c1743a1e7df9f36986454eb232ac8d3c3c Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Thu, 5 Oct 2023 16:39:08 -0500 Subject: [PATCH 30/53] Add new card fields beta component (#84) --- .../devcontainer.json | 40 +++++ .../welcome-message.sh | 23 +++ advanced-integration/beta/.env.example | 5 + advanced-integration/beta/README.md | 11 ++ .../beta/client/checkout.html | 24 +++ advanced-integration/beta/client/checkout.js | 144 +++++++++++++++++ advanced-integration/beta/package.json | 23 +++ advanced-integration/beta/server/server.js | 152 ++++++++++++++++++ 8 files changed, 422 insertions(+) create mode 100644 .devcontainer/advanced-integration-beta/devcontainer.json create mode 100644 .devcontainer/advanced-integration-beta/welcome-message.sh create mode 100644 advanced-integration/beta/.env.example create mode 100644 advanced-integration/beta/README.md create mode 100644 advanced-integration/beta/client/checkout.html create mode 100644 advanced-integration/beta/client/checkout.js create mode 100644 advanced-integration/beta/package.json create mode 100644 advanced-integration/beta/server/server.js 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 @@ + + + + + + + PayPal JS SDK Advanced Integration - Checkout Flow + + +
+
+
+
+
+
+ +
+

+ + + + + diff --git a/advanced-integration/beta/client/checkout.js b/advanced-integration/beta/client/checkout.js new file mode 100644 index 00000000..517edeb8 --- /dev/null +++ b/advanced-integration/beta/client/checkout.js @@ -0,0 +1,144 @@ +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"); + +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}/`); +}); From ba6914a3b3439ba478c61ce4ea1f8062fcd555c2 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Thu, 5 Oct 2023 16:50:55 -0500 Subject: [PATCH 31/53] Fix codespaces path --- .devcontainer/advanced-integration-beta/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/advanced-integration-beta/devcontainer.json b/.devcontainer/advanced-integration-beta/devcontainer.json index 44c2d10c..818b454f 100644 --- a/.devcontainer/advanced-integration-beta/devcontainer.json +++ b/.devcontainer/advanced-integration-beta/devcontainer.json @@ -4,7 +4,7 @@ "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", + "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. From f62771f2529d0ff5ecf2e92e051871968dcf9d81 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Fri, 6 Oct 2023 10:01:31 -0500 Subject: [PATCH 32/53] Use existing css to center beta card form --- advanced-integration/beta/client/checkout.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/advanced-integration/beta/client/checkout.html b/advanced-integration/beta/client/checkout.html index a052a57a..cbc61169 100644 --- a/advanced-integration/beta/client/checkout.html +++ b/advanced-integration/beta/client/checkout.html @@ -8,8 +8,8 @@ PayPal JS SDK Advanced Integration - Checkout Flow -
-
+
+
From 5a3ea22d61bf6da31b63c6d046c4f3608c07d393 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Fri, 6 Oct 2023 12:36:46 -0500 Subject: [PATCH 33/53] fix(docs): update instructions for replacing client-id in html (#86) --- advanced-integration/beta/README.md | 12 +++++++----- standard-integration/README.md | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/advanced-integration/beta/README.md b/advanced-integration/beta/README.md index 923a5234..2f6e5140 100644 --- a/advanced-integration/beta/README.md +++ b/advanced-integration/beta/README.md @@ -4,8 +4,10 @@ This folder contains example code for an Advanced PayPal integration using both ## 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. 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/standard-integration/README.md b/standard-integration/README.md index c0bf83f3..408396b8 100644 --- a/standard-integration/README.md +++ b/standard-integration/README.md @@ -6,7 +6,7 @@ This folder contains example code for a Standard PayPal integration using both t 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/index.html` with your app's client-id +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 From f240e2692b7876bad75fdc7d063fe833675cce8f Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:32:17 -0500 Subject: [PATCH 34/53] Add codespaces example for hosted fields v1 changes --- .../advanced-integration-v1/devcontainer.json | 40 +++++++++++++++++++ .../welcome-message.sh | 23 +++++++++++ 2 files changed, 63 insertions(+) create mode 100644 .devcontainer/advanced-integration-v1/devcontainer.json create mode 100644 .devcontainer/advanced-integration-v1/welcome-message.sh diff --git a/.devcontainer/advanced-integration-v1/devcontainer.json b/.devcontainer/advanced-integration-v1/devcontainer.json new file mode 100644 index 00000000..62758180 --- /dev/null +++ b/.devcontainer/advanced-integration-v1/devcontainer.json @@ -0,0 +1,40 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "PayPal Advanced Integration (v1)", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration/v1", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash ../../.devcontainer/advanced-integration-v1/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-v1/welcome-message.sh b/.devcontainer/advanced-integration-v1/welcome-message.sh new file mode 100644 index 00000000..ae9a72f9 --- /dev/null +++ b/.devcontainer/advanced-integration-v1/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" From 7c03b1a77659e04876aec873016d2f62c9b7966b Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Thu, 19 Oct 2023 16:51:03 -0500 Subject: [PATCH 35/53] Update the name of the v2 card fields codespaces config (#88) --- .../devcontainer.json | 2 +- .../welcome-message.sh | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename .devcontainer/{advanced-integration-beta => advanced-integration-v2}/devcontainer.json (96%) rename .devcontainer/{advanced-integration-beta => advanced-integration-v2}/welcome-message.sh (100%) diff --git a/.devcontainer/advanced-integration-beta/devcontainer.json b/.devcontainer/advanced-integration-v2/devcontainer.json similarity index 96% rename from .devcontainer/advanced-integration-beta/devcontainer.json rename to .devcontainer/advanced-integration-v2/devcontainer.json index 818b454f..b3a54490 100644 --- a/.devcontainer/advanced-integration-beta/devcontainer.json +++ b/.devcontainer/advanced-integration-v2/devcontainer.json @@ -1,6 +1,6 @@ // For more details, see https://aka.ms/devcontainer.json. { - "name": "PayPal Advanced Integration (beta)", + "name": "PayPal Advanced Integration (v2)", "image": "mcr.microsoft.com/devcontainers/javascript-node:20", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration/beta", // Use 'onCreateCommand' to run commands when creating the container. diff --git a/.devcontainer/advanced-integration-beta/welcome-message.sh b/.devcontainer/advanced-integration-v2/welcome-message.sh similarity index 100% rename from .devcontainer/advanced-integration-beta/welcome-message.sh rename to .devcontainer/advanced-integration-v2/welcome-message.sh From 4fe6e5ad42cf1e93852ec246701d57344ceafc31 Mon Sep 17 00:00:00 2001 From: cnallam <130782580+cnallam@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:10:16 +0000 Subject: [PATCH 36/53] My Changes --- standard-integration/server/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard-integration/server/server.js b/standard-integration/server/server.js index 0d8d3cb8..fba19829 100644 --- a/standard-integration/server/server.js +++ b/standard-integration/server/server.js @@ -59,7 +59,7 @@ const createOrder = async (cart) => { { amount: { currency_code: "USD", - value: "100.00", + value: "110.00", }, }, ], From 39aeb3cb00899f1354ce1ec010952fdbae73a944 Mon Sep 17 00:00:00 2001 From: "Devon Apple (PayPal)" <105875840+devapplepaypal@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:35:46 -0700 Subject: [PATCH 37/53] update advanced integration format to support v1 and v2 (#87) --- advanced-integration/README.md | 9 +- advanced-integration/beta/README.md | 13 -- advanced-integration/package.json | 24 --- advanced-integration/v1/.gitignore | 1 + advanced-integration/v1/README.md | 15 +- advanced-integration/v1/client/app.js | 186 ------------------ .../{.env.example => v1/env.example} | 0 advanced-integration/v1/package.json | 4 +- advanced-integration/{ => v1}/paypal-api.js | 0 advanced-integration/{ => v1}/public/app.js | 0 advanced-integration/{ => v1}/server.js | 0 advanced-integration/v1/server/server.js | 178 ----------------- .../v1/{server => }/views/checkout.ejs | 31 ++- .../{beta => v2}/.env.example | 0 advanced-integration/v2/README.md | 23 +++ .../{beta => v2}/client/checkout.html | 0 .../{beta => v2}/client/checkout.js | 0 .../{beta => v2}/package.json | 0 .../{beta => v2}/server/server.js | 0 19 files changed, 56 insertions(+), 428 deletions(-) delete mode 100644 advanced-integration/beta/README.md delete mode 100644 advanced-integration/package.json create mode 100644 advanced-integration/v1/.gitignore delete mode 100644 advanced-integration/v1/client/app.js rename advanced-integration/{.env.example => v1/env.example} (100%) rename advanced-integration/{ => v1}/paypal-api.js (100%) rename advanced-integration/{ => v1}/public/app.js (100%) rename advanced-integration/{ => v1}/server.js (100%) delete mode 100644 advanced-integration/v1/server/server.js rename advanced-integration/v1/{server => }/views/checkout.ejs (88%) rename advanced-integration/{beta => v2}/.env.example (100%) create mode 100644 advanced-integration/v2/README.md rename advanced-integration/{beta => v2}/client/checkout.html (100%) rename advanced-integration/{beta => v2}/client/checkout.js (100%) rename advanced-integration/{beta => v2}/package.json (100%) rename advanced-integration/{beta => v2}/server/server.js (100%) 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 @@ - - + - - - PayPal JS SDK Advanced Integration + + + /> diff --git a/advanced-integration/beta/.env.example b/advanced-integration/v2/.env.example similarity index 100% rename from advanced-integration/beta/.env.example rename to advanced-integration/v2/.env.example diff --git a/advanced-integration/v2/README.md b/advanced-integration/v2/README.md new file mode 100644 index 00000000..fc665be3 --- /dev/null +++ b/advanced-integration/v2/README.md @@ -0,0 +1,23 @@ +# Advanced Integration Example + +This folder contains example code for [version 2](https://developer.paypal.com/docs/checkout/advanced/integrate/) of a PayPal advanced Checkout integration using the JavaScript SDK and Node.js to complete transactions with the PayPal REST API. + +Version 2 is the current advanced Checkout integration, and includes hosted card fields. + +## 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) + +## Examples + +The documentation for advanced Checkout integration using JavaScript SDK includes additional sample code in the following sections: + +* **3. Adding PayPal buttons and card fields** includes [a full-stack Node.js example](v2/examples/full-stack/). +* **4. Call Orders API for PayPal buttons and card fields** includes [a server-side example](v2/examples/call-orders-api-server-side/) +* **5. Capture order** includes [a server-side example](v2/examples/capture-order-server-side/) diff --git a/advanced-integration/beta/client/checkout.html b/advanced-integration/v2/client/checkout.html similarity index 100% rename from advanced-integration/beta/client/checkout.html rename to advanced-integration/v2/client/checkout.html diff --git a/advanced-integration/beta/client/checkout.js b/advanced-integration/v2/client/checkout.js similarity index 100% rename from advanced-integration/beta/client/checkout.js rename to advanced-integration/v2/client/checkout.js diff --git a/advanced-integration/beta/package.json b/advanced-integration/v2/package.json similarity index 100% rename from advanced-integration/beta/package.json rename to advanced-integration/v2/package.json diff --git a/advanced-integration/beta/server/server.js b/advanced-integration/v2/server/server.js similarity index 100% rename from advanced-integration/beta/server/server.js rename to advanced-integration/v2/server/server.js From 8895d079bd2077b913d2d901dcd9154e04366ab2 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:42:21 -0500 Subject: [PATCH 38/53] Update codespaces links to support v1 and v2 (#89) --- .../advanced-integration/devcontainer.json | 40 ------------------- .../advanced-integration/welcome-message.sh | 23 ----------- README.md | 3 +- 3 files changed, 2 insertions(+), 64 deletions(-) delete mode 100644 .devcontainer/advanced-integration/devcontainer.json delete mode 100644 .devcontainer/advanced-integration/welcome-message.sh diff --git a/.devcontainer/advanced-integration/devcontainer.json b/.devcontainer/advanced-integration/devcontainer.json deleted file mode 100644 index 2e840f6f..00000000 --- a/.devcontainer/advanced-integration/devcontainer.json +++ /dev/null @@ -1,40 +0,0 @@ -// For more details, see https://aka.ms/devcontainer.json. -{ - "name": "PayPal Advanced Integration", - "image": "mcr.microsoft.com/devcontainers/javascript-node:20", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration", - // Use 'onCreateCommand' to run commands when creating the container. - "onCreateCommand": "bash ../.devcontainer/advanced-integration/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/welcome-message.sh b/.devcontainer/advanced-integration/welcome-message.sh deleted file mode 100644 index ae9a72f9..00000000 --- a/.devcontainer/advanced-integration/welcome-message.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/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/README.md b/README.md index b0bba29e..6a5e4692 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,8 @@ PayPal codespaces require a client ID and client secret for your app. | Application | Codespaces Link | | ---- | ---- | -| Advanced Integration | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fadvanced-integration%2Fdevcontainer.json)| +| Advanced Integration v2 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fadvanced-integration-v2%2Fdevcontainer.json)| +| Advanced Integration v1 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fadvanced-integration-v1%2Fdevcontainer.json)| | Standard Integration | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fstandard-integration%2Fdevcontainer.json)| ### Learn more From 91683d348eb0235f101e22d7a94788bd9cf8770b Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:47:32 -0500 Subject: [PATCH 39/53] Clean up advanced-integration directory (#90) --- advanced-integration/v1/package.json | 2 +- advanced-integration/{ => v2}/.gitignore | 0 advanced-integration/views/checkout.ejs | 101 ----------------------- 3 files changed, 1 insertion(+), 102 deletions(-) rename advanced-integration/{ => v2}/.gitignore (100%) delete mode 100644 advanced-integration/views/checkout.ejs diff --git a/advanced-integration/v1/package.json b/advanced-integration/v1/package.json index d17aa000..e1b47190 100644 --- a/advanced-integration/v1/package.json +++ b/advanced-integration/v1/package.json @@ -2,7 +2,7 @@ "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", + "main": "server.js", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", diff --git a/advanced-integration/.gitignore b/advanced-integration/v2/.gitignore similarity index 100% rename from advanced-integration/.gitignore rename to advanced-integration/v2/.gitignore diff --git a/advanced-integration/views/checkout.ejs b/advanced-integration/views/checkout.ejs deleted file mode 100644 index 12522326..00000000 --- a/advanced-integration/views/checkout.ejs +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - -
-
-
- -
-
-
- -
-
-
- -
-
-
- - -
- - -
-
- - -
-
- -
-
- -
-
- -
-
- -
-

- -
-
- - - From e453180421cfaa9359ccf0cd5531318074c17af6 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Wed, 25 Oct 2023 10:01:12 -0500 Subject: [PATCH 40/53] Fix codespace path for advanced integration v2 (#92) --- .devcontainer/advanced-integration-v2/devcontainer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/advanced-integration-v2/devcontainer.json b/.devcontainer/advanced-integration-v2/devcontainer.json index b3a54490..703549f5 100644 --- a/.devcontainer/advanced-integration-v2/devcontainer.json +++ b/.devcontainer/advanced-integration-v2/devcontainer.json @@ -2,9 +2,9 @@ { "name": "PayPal Advanced Integration (v2)", "image": "mcr.microsoft.com/devcontainers/javascript-node:20", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration/beta", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration/v2", // Use 'onCreateCommand' to run commands when creating the container. - "onCreateCommand": "bash ../../.devcontainer/advanced-integration-beta/welcome-message.sh", + "onCreateCommand": "bash ../../.devcontainer/advanced-integration-v2/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. From c4ee30fb101f3b2ea4a3665a37c7beca65fb02b5 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Wed, 25 Oct 2023 12:14:00 -0500 Subject: [PATCH 41/53] Add ejs to better match the v2 advanced integration docs (#93) --- advanced-integration/v2/README.md | 6 +++--- advanced-integration/v2/package.json | 1 + advanced-integration/v2/server/server.js | 16 ++++++++++++---- .../checkout.html => server/views/checkout.ejs} | 3 +-- 4 files changed, 17 insertions(+), 9 deletions(-) rename advanced-integration/v2/{client/checkout.html => server/views/checkout.ejs} (89%) diff --git a/advanced-integration/v2/README.md b/advanced-integration/v2/README.md index fc665be3..2cf823b0 100644 --- a/advanced-integration/v2/README.md +++ b/advanced-integration/v2/README.md @@ -18,6 +18,6 @@ Version 2 is the current advanced Checkout integration, and includes hosted card The documentation for advanced Checkout integration using JavaScript SDK includes additional sample code in the following sections: -* **3. Adding PayPal buttons and card fields** includes [a full-stack Node.js example](v2/examples/full-stack/). -* **4. Call Orders API for PayPal buttons and card fields** includes [a server-side example](v2/examples/call-orders-api-server-side/) -* **5. Capture order** includes [a server-side example](v2/examples/capture-order-server-side/) +- **3. Adding PayPal buttons and card fields** includes [a full-stack Node.js example](v2/examples/full-stack/). +- **4. Call Orders API for PayPal buttons and card fields** includes [a server-side example](v2/examples/call-orders-api-server-side/) +- **5. Capture order** includes [a server-side example](v2/examples/capture-order-server-side/) diff --git a/advanced-integration/v2/package.json b/advanced-integration/v2/package.json index c87347ac..ff3f5b41 100644 --- a/advanced-integration/v2/package.json +++ b/advanced-integration/v2/package.json @@ -14,6 +14,7 @@ "license": "Apache-2.0", "dependencies": { "dotenv": "^16.3.1", + "ejs": "^3.1.9", "express": "^4.18.2", "node-fetch": "^3.3.2" }, diff --git a/advanced-integration/v2/server/server.js b/advanced-integration/v2/server/server.js index 0d8d3cb8..f1188758 100644 --- a/advanced-integration/v2/server/server.js +++ b/advanced-integration/v2/server/server.js @@ -1,12 +1,14 @@ 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(); +app.set("view engine", "ejs"); +app.set("views", "./server/views"); + // host static files app.use(express.static("client")); @@ -142,9 +144,15 @@ app.post("/api/orders/:orderID/capture", async (req, res) => { } }); -// serve index.html -app.get("/", (req, res) => { - res.sendFile(path.resolve("./client/checkout.html")); +// render checkout page with client id & unique client token +app.get("/", async (req, res) => { + try { + res.render("checkout", { + clientId: PAYPAL_CLIENT_ID, + }); + } catch (err) { + res.status(500).send(err.message); + } }); app.listen(PORT, () => { diff --git a/advanced-integration/v2/client/checkout.html b/advanced-integration/v2/server/views/checkout.ejs similarity index 89% rename from advanced-integration/v2/client/checkout.html rename to advanced-integration/v2/server/views/checkout.ejs index cbc61169..2b685ad6 100644 --- a/advanced-integration/v2/client/checkout.html +++ b/advanced-integration/v2/server/views/checkout.ejs @@ -17,8 +17,7 @@

- - + From 4603a5180889611fe9f66acd6c89e03b34576e02 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Thu, 26 Oct 2023 09:58:18 -0500 Subject: [PATCH 42/53] Update instructions for v2 card fields (#97) --- advanced-integration/v2/README.md | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/advanced-integration/v2/README.md b/advanced-integration/v2/README.md index 2cf823b0..40cd1bd6 100644 --- a/advanced-integration/v2/README.md +++ b/advanced-integration/v2/README.md @@ -8,16 +8,7 @@ Version 2 is the current advanced Checkout integration, and includes hosted card 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) - -## Examples - -The documentation for advanced Checkout integration using JavaScript SDK includes additional sample code in the following sections: - -- **3. Adding PayPal buttons and card fields** includes [a full-stack Node.js example](v2/examples/full-stack/). -- **4. Call Orders API for PayPal buttons and card fields** includes [a server-side example](v2/examples/call-orders-api-server-side/) -- **5. Capture order** includes [a server-side example](v2/examples/capture-order-server-side/) +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) From 4f82e502daad966285dcc49f1f21eae059e33764 Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Mon, 13 Nov 2023 15:34:15 -0600 Subject: [PATCH 43/53] feat: add example for saving a payment method (#99) --- save-payment-method/.env.example | 5 + save-payment-method/.gitignore | 1 + save-payment-method/README.md | 15 ++ save-payment-method/client/app.js | 97 +++++++++ save-payment-method/package.json | 24 +++ save-payment-method/server/server.js | 193 ++++++++++++++++++ save-payment-method/server/views/checkout.ejs | 17 ++ 7 files changed, 352 insertions(+) create mode 100644 save-payment-method/.env.example create mode 100644 save-payment-method/.gitignore create mode 100644 save-payment-method/README.md create mode 100644 save-payment-method/client/app.js create mode 100644 save-payment-method/package.json create mode 100644 save-payment-method/server/server.js create mode 100644 save-payment-method/server/views/checkout.ejs 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 @@ + + + + + + PayPal JS SDK Save Payment Method Integration + + +
+

+ + + + From 31e5b51e67e693ca802ae54a8fb25287bbf69ec7 Mon Sep 17 00:00:00 2001 From: Navinkumar Patil <134013160+NavinPayPal@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:28:54 -0800 Subject: [PATCH 44/53] Update devcontainer.json with paypal vs code extension (#106) --- .devcontainer/advanced-integration-v2/devcontainer.json | 2 +- .devcontainer/standard-integration/devcontainer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/advanced-integration-v2/devcontainer.json b/.devcontainer/advanced-integration-v2/devcontainer.json index 703549f5..9a4ab9ac 100644 --- a/.devcontainer/advanced-integration-v2/devcontainer.json +++ b/.devcontainer/advanced-integration-v2/devcontainer.json @@ -31,7 +31,7 @@ }, "customizations": { "vscode": { - "extensions": ["vsls-contrib.codetour"], + "extensions": ["vsls-contrib.codetour","PayPal.vscode-paypal"], "settings": { "git.openRepositoryInParentFolders": "always" } diff --git a/.devcontainer/standard-integration/devcontainer.json b/.devcontainer/standard-integration/devcontainer.json index 846dd982..c6d0ffec 100644 --- a/.devcontainer/standard-integration/devcontainer.json +++ b/.devcontainer/standard-integration/devcontainer.json @@ -31,7 +31,7 @@ }, "customizations": { "vscode": { - "extensions": ["vsls-contrib.codetour"], + "extensions": ["vsls-contrib.codetour","PayPal.vscode-paypal"], "settings": { "git.openRepositoryInParentFolders": "always" } From 6419cb99349318f6eb89e657779b9f8c769cfe4d Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Wed, 20 Dec 2023 16:45:54 -0600 Subject: [PATCH 45/53] Update hosted-fields example to new standards (#109) --- advanced-integration/v1/client/app.js | 186 ++++++++++++++++++ advanced-integration/v1/package.json | 6 +- advanced-integration/v1/paypal-api.js | 100 ---------- advanced-integration/v1/public/app.js | 160 --------------- advanced-integration/v1/server.js | 44 ----- advanced-integration/v1/server/server.js | 178 +++++++++++++++++ .../v1/server/views/checkout.ejs | 104 ++++++++++ 7 files changed, 471 insertions(+), 307 deletions(-) create mode 100644 advanced-integration/v1/client/app.js delete mode 100644 advanced-integration/v1/paypal-api.js delete mode 100644 advanced-integration/v1/public/app.js delete mode 100644 advanced-integration/v1/server.js create mode 100644 advanced-integration/v1/server/server.js create mode 100644 advanced-integration/v1/server/views/checkout.ejs diff --git a/advanced-integration/v1/client/app.js b/advanced-integration/v1/client/app.js new file mode 100644 index 00000000..65f048d7 --- /dev/null +++ b/advanced-integration/v1/client/app.js @@ -0,0 +1,186 @@ +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/v1/package.json b/advanced-integration/v1/package.json index e1b47190..ff3f5b41 100644 --- a/advanced-integration/v1/package.json +++ b/advanced-integration/v1/package.json @@ -2,14 +2,14 @@ "name": "paypal-advanced-integration", "description": "Sample Node.js web app to integrate PayPal Advanced Checkout for online payments", "version": "1.0.0", - "main": "server.js", + "main": "server/server.js", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "nodemon server.js", + "start": "nodemon server/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" + "lint": "npx eslint server/*.js --env=node && npx eslint client/*.js --env=browser" }, "license": "Apache-2.0", "dependencies": { diff --git a/advanced-integration/v1/paypal-api.js b/advanced-integration/v1/paypal-api.js deleted file mode 100644 index 6e6c8aaf..00000000 --- a/advanced-integration/v1/paypal-api.js +++ /dev/null @@ -1,100 +0,0 @@ -import fetch from "node-fetch"; - -// set some important variables -const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET } = process.env; -const base = "https://api-m.sandbox.paypal.com"; - -/** - * Create an order - * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create - */ -export async function createOrder() { - const purchaseAmount = "100.00"; // TODO: pull prices from a database - const accessToken = await generateAccessToken(); - const url = `${base}/v2/checkout/orders`; - const response = await fetch(url, { - method: "post", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - intent: "CAPTURE", - purchase_units: [ - { - amount: { - currency_code: "USD", - value: purchaseAmount, - }, - }, - ], - }), - }); - - return handleResponse(response); -} - -/** - * Capture payment for an order - * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture - */ -export async function capturePayment(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}`, - }, - }); - - return handleResponse(response); -} - -/** - * Generate an OAuth 2.0 access token - * @see https://developer.paypal.com/api/rest/authentication/ - */ -export async function generateAccessToken() { - 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 jsonData = await handleResponse(response); - return jsonData.access_token; -} - -/** - * Generate a client token - * @see https://developer.paypal.com/docs/checkout/advanced/integrate/#link-sampleclienttokenrequest - */ -export async function generateClientToken() { - const accessToken = await generateAccessToken(); - const response = await fetch(`${base}/v1/identity/generate-token`, { - method: "post", - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Language": "en_US", - "Content-Type": "application/json", - }, - }); - console.log("response", response.status); - const jsonData = await handleResponse(response); - return jsonData.client_token; -} - -async function handleResponse(response) { - if (response.status === 200 || response.status === 201) { - return response.json(); - } - - const errorMessage = await response.text(); - throw new Error(errorMessage); -} diff --git a/advanced-integration/v1/public/app.js b/advanced-integration/v1/public/app.js deleted file mode 100644 index f475472d..00000000 --- a/advanced-integration/v1/public/app.js +++ /dev/null @@ -1,160 +0,0 @@ -window.paypal - .Buttons({ - // Sets up the transaction when a payment button is clicked - createOrder: function () { - return fetch("/api/orders", { - method: "post", - // use the "body" param to optionally pass additional order information - // like product skus and quantities - body: JSON.stringify({ - cart: [ - { - sku: "", - quantity: "", - }, - ], - }), - }) - .then((response) => response.json()) - .then((order) => order.id); - }, - // Finalize the transaction after payer approval - onApprove: function (data) { - return fetch(`/api/orders/${data.orderID}/capture`, { - method: "post", - }) - .then((response) => response.json()) - .then((orderData) => { - // Successful capture! For dev/demo purposes: - console.log( - "Capture result", - orderData, - JSON.stringify(orderData, null, 2), - ); - const transaction = orderData.purchase_units[0].payments.captures[0]; - alert(`Transaction ${transaction.status}: ${transaction.id} - - See console for all available details - `); - // When ready to go live, remove the alert and show a success message within this page. For example: - // var element = document.getElementById('paypal-button-container'); - // element.innerHTML = '

Thank you for your payment!

'; - // Or go to another URL: actions.redirect('thank_you.html'); - }); - }, - }) - .render("#paypal-button-container"); - -// If this returns false or the card fields aren't visible, see Step #1. -if (window.paypal.HostedFields.isEligible()) { - let orderId; - - // Renders card fields - window.paypal.HostedFields.render({ - // Call your server to set up the transaction - createOrder: () => { - return fetch("/api/orders", { - method: "post", - // use the "body" param to optionally pass additional order information - // like product skus and quantities - body: JSON.stringify({ - cart: [ - { - sku: "", - quantity: "", - }, - ], - }), - }) - .then((res) => res.json()) - .then((orderData) => { - orderId = orderData.id; // needed later to complete capture - return orderData.id; - }); - }, - 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(() => { - fetch(`/api/orders/${orderId}/capture`, { - method: "post", - }) - .then((res) => res.json()) - .then((orderData) => { - // Two cases to handle: - // (1) Other non-recoverable errors -> Show a failure message - // (2) Successful transaction -> Show confirmation or thank you - // This example reads a v2/checkout/orders capture response, propagated from the server - // You could use a different API or structure for your 'orderData' - const errorDetail = - Array.isArray(orderData.details) && orderData.details[0]; - if (errorDetail) { - var msg = "Sorry, your transaction could not be processed."; - if (errorDetail.description) - msg += "\n\n" + errorDetail.description; - if (orderData.debug_id) msg += " (" + orderData.debug_id + ")"; - return alert(msg); // Show a failure message - } - // Show a success message or redirect - alert("Transaction completed!"); - }); - }) - .catch((err) => { - alert("Payment could not be captured! " + JSON.stringify(err)); - }); - }); - }); -} else { - // Hides card fields if the merchant isn't eligible - document.querySelector("#card-form").style = "display: none"; -} diff --git a/advanced-integration/v1/server.js b/advanced-integration/v1/server.js deleted file mode 100644 index 73076fcb..00000000 --- a/advanced-integration/v1/server.js +++ /dev/null @@ -1,44 +0,0 @@ -import "dotenv/config"; -import express from "express"; -import * as paypal from "./paypal-api.js"; -const { PORT = 8888 } = process.env; - -const app = express(); -app.set("view engine", "ejs"); -app.use(express.static("public")); - -// render checkout page with client id & unique client token -app.get("/", async (req, res) => { - const clientId = process.env.PAYPAL_CLIENT_ID; - try { - const clientToken = await paypal.generateClientToken(); - res.render("checkout", { clientId, clientToken }); - } catch (err) { - res.status(500).send(err.message); - } -}); - -// create order -app.post("/api/orders", async (req, res) => { - try { - const order = await paypal.createOrder(); - res.json(order); - } catch (err) { - res.status(500).send(err.message); - } -}); - -// capture payment -app.post("/api/orders/:orderID/capture", async (req, res) => { - const { orderID } = req.params; - try { - const captureData = await paypal.capturePayment(orderID); - res.json(captureData); - } catch (err) { - res.status(500).send(err.message); - } -}); - -app.listen(PORT, () => { - console.log(`Server listening at http://localhost:${PORT}/`); -}); diff --git a/advanced-integration/v1/server/server.js b/advanced-integration/v1/server/server.js new file mode 100644 index 00000000..a7d84407 --- /dev/null +++ b/advanced-integration/v1/server/server.js @@ -0,0 +1,178 @@ +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/server/views/checkout.ejs new file mode 100644 index 00000000..85cd7085 --- /dev/null +++ b/advanced-integration/v1/server/views/checkout.ejs @@ -0,0 +1,104 @@ + + + + + + PayPal JS SDK Advanced Integration + + + + +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+

+ +
+

+
+ + + From ecc5ab54f74b424054825b860f91accc76621fbf Mon Sep 17 00:00:00 2001 From: Navin Patil Date: Wed, 10 Jan 2024 15:04:43 -0800 Subject: [PATCH 46/53] Adding changes for devcontainer file for save payment method --- .../advanced-integration-v1/devcontainer.json | 2 +- .../save-payment-method/devcontainer.json | 40 +++++++++++++++++++ .../save-payment-method/welcome-message.sh | 23 +++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/save-payment-method/devcontainer.json create mode 100644 .devcontainer/save-payment-method/welcome-message.sh diff --git a/.devcontainer/advanced-integration-v1/devcontainer.json b/.devcontainer/advanced-integration-v1/devcontainer.json index 62758180..b8f1d538 100644 --- a/.devcontainer/advanced-integration-v1/devcontainer.json +++ b/.devcontainer/advanced-integration-v1/devcontainer.json @@ -31,7 +31,7 @@ }, "customizations": { "vscode": { - "extensions": ["vsls-contrib.codetour"], + "extensions": ["vsls-contrib.codetour","PayPal.vscode-paypal"], "settings": { "git.openRepositoryInParentFolders": "always" } diff --git a/.devcontainer/save-payment-method/devcontainer.json b/.devcontainer/save-payment-method/devcontainer.json new file mode 100644 index 00000000..c6d0ffec --- /dev/null +++ b/.devcontainer/save-payment-method/devcontainer.json @@ -0,0 +1,40 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "PayPal Standard Integration", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/standard-integration", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash ../.devcontainer/standard-integration/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 Standard 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","PayPal.vscode-paypal"], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + } +} diff --git a/.devcontainer/save-payment-method/welcome-message.sh b/.devcontainer/save-payment-method/welcome-message.sh new file mode 100644 index 00000000..7ed0d59c --- /dev/null +++ b/.devcontainer/save-payment-method/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Save Payment Method 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 Save Payment Method 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" \ No newline at end of file From e385dc5a17baac293d6cd26615c422b43fac9406 Mon Sep 17 00:00:00 2001 From: Navin Patil Date: Wed, 10 Jan 2024 16:13:35 -0800 Subject: [PATCH 47/53] Updating Readme file to add Codespaces button --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6a5e4692..daf0636c 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ PayPal codespaces require a client ID and client secret for your app. | Advanced Integration v2 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fadvanced-integration-v2%2Fdevcontainer.json)| | Advanced Integration v1 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fadvanced-integration-v1%2Fdevcontainer.json)| | Standard Integration | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fstandard-integration%2Fdevcontainer.json)| +| Save Payment Method Integration | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fsave-payment-method%2Fdevcontainer.json)| ### Learn more From a354698603378c88eebde44fc27edad8d3e3e78a Mon Sep 17 00:00:00 2001 From: Navin Patil Date: Tue, 16 Jan 2024 15:45:54 -0800 Subject: [PATCH 48/53] Updating the devcontainer file --- .devcontainer/save-payment-method/devcontainer.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.devcontainer/save-payment-method/devcontainer.json b/.devcontainer/save-payment-method/devcontainer.json index c6d0ffec..e3d1cede 100644 --- a/.devcontainer/save-payment-method/devcontainer.json +++ b/.devcontainer/save-payment-method/devcontainer.json @@ -1,10 +1,11 @@ + // For more details, see https://aka.ms/devcontainer.json. { - "name": "PayPal Standard Integration", + "name": "PayPal Save Payment Method", "image": "mcr.microsoft.com/devcontainers/javascript-node:20", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/standard-integration", - // Use 'onCreateCommand' to run commands when creating the container. - "onCreateCommand": "bash ../.devcontainer/standard-integration/welcome-message.sh", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/save-payment-method", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash ../.devcontainer/save-payment-method/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. @@ -15,7 +16,7 @@ "forwardPorts": [8888], "portsAttributes": { "8888": { - "label": "Preview of Standard Checkout Flow", + "label": "Preview of Save Payment Method Flow", "onAutoForward": "openBrowserOnce" } }, @@ -37,4 +38,4 @@ } } } -} +} \ No newline at end of file From a2074e9e49e0cf0bd8e98d472b0267ddc245b87d Mon Sep 17 00:00:00 2001 From: Navin Patil Date: Tue, 16 Jan 2024 16:23:54 -0800 Subject: [PATCH 49/53] Updating devcontainer file for save payment --- .devcontainer/save-payment-method/devcontainer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.devcontainer/save-payment-method/devcontainer.json b/.devcontainer/save-payment-method/devcontainer.json index e3d1cede..ec88eb6c 100644 --- a/.devcontainer/save-payment-method/devcontainer.json +++ b/.devcontainer/save-payment-method/devcontainer.json @@ -1,10 +1,9 @@ - // For more details, see https://aka.ms/devcontainer.json. { "name": "PayPal Save Payment Method", "image": "mcr.microsoft.com/devcontainers/javascript-node:20", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/save-payment-method", - // Use 'onCreateCommand' to run commands when creating the container. + // Use 'onCreateCommand' to run commands when creating the container. "onCreateCommand": "bash ../.devcontainer/save-payment-method/welcome-message.sh", // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": "npm install", From 633fe7eaca934d3931338754945ea04c8068d6c3 Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Thu, 11 Apr 2024 16:07:55 -0500 Subject: [PATCH 50/53] fix one click payment popup issue (#126) --- save-payment-method/server/views/checkout.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/save-payment-method/server/views/checkout.ejs b/save-payment-method/server/views/checkout.ejs index fa995630..69cd3e78 100644 --- a/save-payment-method/server/views/checkout.ejs +++ b/save-payment-method/server/views/checkout.ejs @@ -9,7 +9,7 @@

From bd8f5094f74dc6f7d30271dc4a7c2763dab5f253 Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Fri, 12 Apr 2024 09:16:55 -0500 Subject: [PATCH 51/53] fix lint failure (#129) --- .github/workflows/validate.yml | 2 +- advanced-integration/v1/package.json | 2 +- advanced-integration/v2/package.json | 2 +- save-payment-method/package.json | 2 +- standard-integration/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ba842418..9ae8d9e3 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -29,7 +29,7 @@ jobs: - name: 👕 Lint code with ESLint run: > - find . -name package.json -maxdepth 2 -type f | while read -r file; do + find . -name package.json -maxdepth 3 -type f | while read -r file; do directory=$(dirname "$file") cd "$directory" && npm run lint && cd - done diff --git a/advanced-integration/v1/package.json b/advanced-integration/v1/package.json index ff3f5b41..0e93024f 100644 --- a/advanced-integration/v1/package.json +++ b/advanced-integration/v1/package.json @@ -9,7 +9,7 @@ "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" + "lint": "npx eslint server/*.js client/*.js --no-config-lookup" }, "license": "Apache-2.0", "dependencies": { diff --git a/advanced-integration/v2/package.json b/advanced-integration/v2/package.json index ff3f5b41..0e93024f 100644 --- a/advanced-integration/v2/package.json +++ b/advanced-integration/v2/package.json @@ -9,7 +9,7 @@ "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" + "lint": "npx eslint server/*.js client/*.js --no-config-lookup" }, "license": "Apache-2.0", "dependencies": { diff --git a/save-payment-method/package.json b/save-payment-method/package.json index 858c68af..d1596b09 100644 --- a/save-payment-method/package.json +++ b/save-payment-method/package.json @@ -9,7 +9,7 @@ "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" + "lint": "npx eslint server/*.js client/*.js --no-config-lookup" }, "license": "Apache-2.0", "dependencies": { diff --git a/standard-integration/package.json b/standard-integration/package.json index 5413d87e..f00bfb37 100644 --- a/standard-integration/package.json +++ b/standard-integration/package.json @@ -9,7 +9,7 @@ "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" + "lint": "npx eslint server/*.js client/*.js --no-config-lookup" }, "license": "Apache-2.0", "dependencies": { From a70265b6d4b0212c1d6adda32e49d59e419e1655 Mon Sep 17 00:00:00 2001 From: Mervin Choun <38122192+mchoun@users.noreply.github.com> Date: Mon, 3 Jun 2024 11:53:40 -0400 Subject: [PATCH 52/53] feat: update Card Fields integration with Billing Address (#131) --- advanced-integration/README.md | 4 +-- advanced-integration/v2/README.md | 2 +- advanced-integration/v2/client/checkout.js | 34 +++++++++++++++---- .../v2/server/views/checkout.ejs | 32 +++++++++++++++-- 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/advanced-integration/README.md b/advanced-integration/README.md index 96d2c205..ef1dd69d 100644 --- a/advanced-integration/README.md +++ b/advanced-integration/README.md @@ -2,8 +2,8 @@ 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. +- [`v2`](v2/README.md) contains sample code for the current advanced Checkout integration. This includes guidance on using Card Fields. +- [`v1`](v1/README.md) contains sample code for the legacy advanced Checkout integration. Use `v2` for new integrations. ## Instructions diff --git a/advanced-integration/v2/README.md b/advanced-integration/v2/README.md index 40cd1bd6..332d0d88 100644 --- a/advanced-integration/v2/README.md +++ b/advanced-integration/v2/README.md @@ -2,7 +2,7 @@ This folder contains example code for [version 2](https://developer.paypal.com/docs/checkout/advanced/integrate/) of a PayPal advanced Checkout integration using the JavaScript SDK and Node.js to complete transactions with the PayPal REST API. -Version 2 is the current advanced Checkout integration, and includes hosted card fields. +Version 2 is the current advanced Checkout integration, and includes Card Fields. ## Instructions diff --git a/advanced-integration/v2/client/checkout.js b/advanced-integration/v2/client/checkout.js index 517edeb8..29d9310c 100644 --- a/advanced-integration/v2/client/checkout.js +++ b/advanced-integration/v2/client/checkout.js @@ -124,13 +124,35 @@ if (cardField.isEligible()) { // Add click listener to submit button and call the submit function on the CardField component document - .getElementById("multi-card-field-button") + .getElementById("card-field-submit-button") .addEventListener("click", () => { - cardField.submit().catch((error) => { - resultMessage( - `Sorry, your transaction could not be processed...

${error}`, - ); - }); + cardField + .submit({ + // From your billing address fields + billingAddress: { + addressLine1: document.getElementById("card-billing-address-line-1") + .value, + addressLine2: document.getElementById("card-billing-address-line-2") + .value, + adminArea1: document.getElementById( + "card-billing-address-admin-area-line-1", + ).value, + adminArea2: document.getElementById( + "card-billing-address-admin-area-line-2", + ).value, + countryCode: document.getElementById( + "card-billing-address-country-code", + ).value, + postalCode: document.getElementById( + "card-billing-address-postal-code", + ).value, + }, + }) + .catch((error) => { + resultMessage( + `Sorry, your transaction could not be processed...

${error}`, + ); + }); }); } else { // Hides card fields if the merchant isn't eligible diff --git a/advanced-integration/v2/server/views/checkout.ejs b/advanced-integration/v2/server/views/checkout.ejs index 2b685ad6..5d129f60 100644 --- a/advanced-integration/v2/server/views/checkout.ejs +++ b/advanced-integration/v2/server/views/checkout.ejs @@ -9,15 +9,43 @@
+
- + +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+

+

- + \ No newline at end of file From a2fe43ecf6aba8224c734b57d2f11406bdbb6a47 Mon Sep 17 00:00:00 2001 From: Desires Co Date: Fri, 2 Aug 2024 01:51:44 -0500 Subject: [PATCH 53/53] Create Luxdesignpay --- standard-integration/client/Luxdesignpay | 1 + 1 file changed, 1 insertion(+) create mode 100644 standard-integration/client/Luxdesignpay diff --git a/standard-integration/client/Luxdesignpay b/standard-integration/client/Luxdesignpay new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/standard-integration/client/Luxdesignpay @@ -0,0 +1 @@ +