From 3c418083ca2f17a2d7fc29fd071d798d356bac5f Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Tue, 31 Oct 2023 16:56:19 -0500 Subject: [PATCH 1/7] Add Save Payment Method --- save-payment-method/.env.example | 5 + save-payment-method/.gitignore | 1 + save-payment-method/README.md | 13 ++ save-payment-method/client/app.js | 119 +++++++++++++++ save-payment-method/client/checkout.html | 13 ++ save-payment-method/package.json | 23 +++ save-payment-method/server/server.js | 182 +++++++++++++++++++++++ 7 files changed, 356 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/client/checkout.html create mode 100644 save-payment-method/package.json create mode 100644 save-payment-method/server/server.js 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..2eea525d --- /dev/null +++ b/save-payment-method/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/save-payment-method/README.md b/save-payment-method/README.md new file mode 100644 index 00000000..2b2d17d7 --- /dev/null +++ b/save-payment-method/README.md @@ -0,0 +1,13 @@ +# 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. + +## 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..bd4bea3a --- /dev/null +++ b/save-payment-method/client/app.js @@ -0,0 +1,119 @@ +const main = async () => { + const user = localStorage.getItem("user"); + const response = await fetch(`/api/id-token?user=${user}`); + const data = await response.json(); + const script = document.createElement("script"); + script.src = + "https://www.paypal.com/sdk/js?client-id=AcYMYjcezEZ75OhlJfN12BGO9ZlWQG-zM7laW-2cI5GHC7s4WlWFr02F48nrRUa2CG8BcJRH7org9KPm"; + script.setAttribute("data-user-id-token", data.id_token); + script.addEventListener("load", () => renderButtons()); + document.head.appendChild(script); +}; + +main(); + +const renderButtons = () => + 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`, + ); + + localStorage.setItem( + "user", + orderData.payment_source.paypal.attributes.vault.customer.id, + ); + + 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/client/checkout.html b/save-payment-method/client/checkout.html new file mode 100644 index 00000000..cb5f758d --- /dev/null +++ b/save-payment-method/client/checkout.html @@ -0,0 +1,13 @@ + + + + + + PayPal JS SDK Standard Integration + + +
+

+ + + diff --git a/save-payment-method/package.json b/save-payment-method/package.json new file mode 100644 index 00000000..1d20bc18 --- /dev/null +++ b/save-payment-method/package.json @@ -0,0 +1,23 @@ +{ + "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", + "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..cd8b53ec --- /dev/null +++ b/save-payment-method/server/server.js @@ -0,0 +1,182 @@ +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 authenticate = async (targetCustomerId) => { + 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"); + + let body = "grant_type=client_credentials&response_type=id_token"; + if (targetCustomerId) { + body += `&target_customer_id=${targetCustomerId}`; + } + + const response = await fetch(`${base}/v1/oauth2/token`, { + method: "POST", + body, + 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(); + console.log("at is", accessToken); + 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", + }, + }, + }, + }; + + 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.get("/api/id-token", async (req, res) => { + const { jsonResponse } = await authenticate(req.query.user); + res.json(jsonResponse); +}); + +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 f2c113afc227aeeb0e0e49bf3a83c5da0e504ac8 Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Tue, 31 Oct 2023 17:00:13 -0500 Subject: [PATCH 2/7] Use test client-id --- save-payment-method/.gitignore | 2 +- save-payment-method/client/app.js | 2 +- save-payment-method/server/server.js | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/save-payment-method/.gitignore b/save-payment-method/.gitignore index 2eea525d..4c49bd78 100644 --- a/save-payment-method/.gitignore +++ b/save-payment-method/.gitignore @@ -1 +1 @@ -.env \ No newline at end of file +.env diff --git a/save-payment-method/client/app.js b/save-payment-method/client/app.js index bd4bea3a..ccb05fda 100644 --- a/save-payment-method/client/app.js +++ b/save-payment-method/client/app.js @@ -4,7 +4,7 @@ const main = async () => { const data = await response.json(); const script = document.createElement("script"); script.src = - "https://www.paypal.com/sdk/js?client-id=AcYMYjcezEZ75OhlJfN12BGO9ZlWQG-zM7laW-2cI5GHC7s4WlWFr02F48nrRUa2CG8BcJRH7org9KPm"; + "https://www.paypal.com/sdk/js?client-id=test"; script.setAttribute("data-user-id-token", data.id_token); script.addEventListener("load", () => renderButtons()); document.head.appendChild(script); diff --git a/save-payment-method/server/server.js b/save-payment-method/server/server.js index cd8b53ec..0423cc32 100644 --- a/save-payment-method/server/server.js +++ b/save-payment-method/server/server.js @@ -61,7 +61,6 @@ const createOrder = async (cart) => { ); const accessToken = await generateAccessToken(); - console.log("at is", accessToken); const url = `${base}/v2/checkout/orders`; const payload = { intent: "CAPTURE", From 849759c65bf6203aa40ce80adee917527def7919 Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Tue, 31 Oct 2023 17:01:10 -0500 Subject: [PATCH 3/7] npm run format --- save-payment-method/client/app.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/save-payment-method/client/app.js b/save-payment-method/client/app.js index ccb05fda..c8786112 100644 --- a/save-payment-method/client/app.js +++ b/save-payment-method/client/app.js @@ -3,8 +3,7 @@ const main = async () => { const response = await fetch(`/api/id-token?user=${user}`); const data = await response.json(); const script = document.createElement("script"); - script.src = - "https://www.paypal.com/sdk/js?client-id=test"; + script.src = "https://www.paypal.com/sdk/js?client-id=test"; script.setAttribute("data-user-id-token", data.id_token); script.addEventListener("load", () => renderButtons()); document.head.appendChild(script); From d581019bd5c412076e5ea000e6ef3014dae51a5d Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Thu, 2 Nov 2023 13:11:58 -0500 Subject: [PATCH 4/7] Use ejs to render data-user-id-token --- save-payment-method/client/app.js | 183 ++++++++---------- save-payment-method/package.json | 1 + save-payment-method/server/server.js | 24 ++- .../views/checkout.ejs} | 6 +- 4 files changed, 102 insertions(+), 112 deletions(-) rename save-payment-method/{client/checkout.html => server/views/checkout.ejs} (59%) diff --git a/save-payment-method/client/app.js b/save-payment-method/client/app.js index c8786112..ef695bc1 100644 --- a/save-payment-method/client/app.js +++ b/save-payment-method/client/app.js @@ -1,115 +1,94 @@ -const main = async () => { - const user = localStorage.getItem("user"); - const response = await fetch(`/api/id-token?user=${user}`); - const data = await response.json(); - const script = document.createElement("script"); - script.src = "https://www.paypal.com/sdk/js?client-id=test"; - script.setAttribute("data-user-id-token", data.id_token); - script.addEventListener("load", () => renderButtons()); - document.head.appendChild(script); -}; +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", + }, + ], + }), + }); -main(); + const orderData = await response.json(); -const renderButtons = () => - 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); + 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}`, - ); + throw new Error(errorMessage); } - }, - async onApprove(data, actions) { - try { - const response = await fetch(`/api/orders/${data.orderID}/capture`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }); + } 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 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`, - ); - - localStorage.setItem( - "user", - orderData.payment_source.paypal.attributes.vault.customer.id, - ); + const errorDetail = orderData?.details?.[0]; - console.log( - "Capture result", - orderData, - JSON.stringify(orderData, null, 2), - ); - } - } catch (error) { - console.error(error); + 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( - `Sorry, your transaction could not be processed...

${error}`, + `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), ); } - }, - }) - .render("#paypal-button-container"); + } 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) { diff --git a/save-payment-method/package.json b/save-payment-method/package.json index 1d20bc18..858c68af 100644 --- a/save-payment-method/package.json +++ b/save-payment-method/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/save-payment-method/server/server.js b/save-payment-method/server/server.js index 0423cc32..dd5ad373 100644 --- a/save-payment-method/server/server.js +++ b/save-payment-method/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")); @@ -143,11 +145,6 @@ async function handleResponse(response) { } } -app.get("/api/id-token", async (req, res) => { - const { jsonResponse } = await authenticate(req.query.user); - res.json(jsonResponse); -}); - app.post("/api/orders", async (req, res) => { try { // use the cart information passed from the front-end to calculate the order amount detals @@ -164,6 +161,7 @@ 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); @@ -171,9 +169,17 @@ 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 & user id token +app.get("/", async (req, res) => { + try { + const { jsonResponse } = await authenticate(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, () => { diff --git a/save-payment-method/client/checkout.html b/save-payment-method/server/views/checkout.ejs similarity index 59% rename from save-payment-method/client/checkout.html rename to save-payment-method/server/views/checkout.ejs index cb5f758d..69cd3e78 100644 --- a/save-payment-method/client/checkout.html +++ b/save-payment-method/server/views/checkout.ejs @@ -3,11 +3,15 @@ - PayPal JS SDK Standard Integration + PayPal JS SDK Save Payment Method Integration

+ From dcffe7f6e478afb050daefaebbdee4e6e1d69e80 Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Thu, 2 Nov 2023 13:13:27 -0500 Subject: [PATCH 5/7] Update save-payment-method/server/server.js Co-authored-by: Greg Jopa <534034+gregjopa@users.noreply.github.com> --- save-payment-method/server/server.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/save-payment-method/server/server.js b/save-payment-method/server/server.js index dd5ad373..bffb522d 100644 --- a/save-payment-method/server/server.js +++ b/save-payment-method/server/server.js @@ -19,7 +19,15 @@ 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 (targetCustomerId) => { +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"); From 8b9ba5ddb74151a44df504f01df6f466177c7aab Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Thu, 2 Nov 2023 13:51:02 -0500 Subject: [PATCH 6/7] Link to return buyer experience --- save-payment-method/client/app.js | 2 +- save-payment-method/server/server.js | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/save-payment-method/client/app.js b/save-payment-method/client/app.js index ef695bc1..eebd2017 100644 --- a/save-payment-method/client/app.js +++ b/save-payment-method/client/app.js @@ -70,7 +70,7 @@ window.paypal orderData?.purchase_units?.[0]?.payments?.authorizations?.[0]; resultMessage( `Transaction ${transaction.status}: ${transaction.id}

See console for all available details.
- See the return buyer experience + See the return buyer experience `, ); diff --git a/save-payment-method/server/server.js b/save-payment-method/server/server.js index bffb522d..7d2596f2 100644 --- a/save-payment-method/server/server.js +++ b/save-payment-method/server/server.js @@ -23,9 +23,9 @@ const authenticate = async (bodyParams) => { const params = { grant_type: "client_credentials", response_type: "id_token", - ...bodyParams + ...bodyParams, }; - + // pass the url encoded value as the body of the post call const urlEncodedParams = new URLSearchParams(params).toString(); try { @@ -36,14 +36,9 @@ const authenticate = async (bodyParams) => { PAYPAL_CLIENT_ID + ":" + PAYPAL_CLIENT_SECRET, ).toString("base64"); - let body = "grant_type=client_credentials&response_type=id_token"; - if (targetCustomerId) { - body += `&target_customer_id=${targetCustomerId}`; - } - const response = await fetch(`${base}/v1/oauth2/token`, { method: "POST", - body, + body: urlEncodedParams, headers: { Authorization: `Basic ${auth}`, }, @@ -94,6 +89,7 @@ const createOrder = async (cart) => { experience_context: { return_url: "http://example.com", cancel_url: "http://example.com", + shipping_preference: "NO_SHIPPING", }, }, }, @@ -180,7 +176,9 @@ app.post("/api/orders/:orderID/capture", async (req, res) => { // render checkout page with client id & user id token app.get("/", async (req, res) => { try { - const { jsonResponse } = await authenticate(req.query.customerID); + const { jsonResponse } = await authenticate({ + target_customer_id: req.query.customerID, + }); res.render("checkout", { clientId: PAYPAL_CLIENT_ID, userIdToken: jsonResponse.id_token, From d4ea5c36f3f425017c2529b47826f94b66cd1286 Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Fri, 3 Nov 2023 09:05:54 -0500 Subject: [PATCH 7/7] Add &vault=true to SDK script --- save-payment-method/README.md | 2 ++ save-payment-method/server/views/checkout.ejs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/save-payment-method/README.md b/save-payment-method/README.md index 2b2d17d7..f1f4a0e9 100644 --- a/save-payment-method/README.md +++ b/save-payment-method/README.md @@ -2,6 +2,8 @@ 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) diff --git a/save-payment-method/server/views/checkout.ejs b/save-payment-method/server/views/checkout.ejs index 69cd3e78..fa995630 100644 --- a/save-payment-method/server/views/checkout.ejs +++ b/save-payment-method/server/views/checkout.ejs @@ -9,7 +9,7 @@