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}/`); });