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