From f23cfa00aa4d32129746bb1d5b2ca0c942a1fbf5 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:26:19 -0500 Subject: [PATCH 1/3] Minor tweaks to standard integration --- standard-integration/README.md | 6 +++--- standard-integration/index.html | 8 +++----- standard-integration/package.json | 3 +-- 3 files changed, 7 insertions(+), 10 deletions(-) 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..8d657d3a 100644 --- a/standard-integration/index.html +++ b/standard-integration/index.html @@ -82,8 +82,7 @@ ); } 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 = @@ -107,10 +106,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 9aca3d6db34b2b83ee6ecdb74d7e44a599c9b5d9 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:30:33 -0500 Subject: [PATCH 2/3] Share JS code between card fields and buttons --- advanced-integration/README.md | 4 +- advanced-integration/package.json | 3 +- advanced-integration/public/app.js | 249 ++++++++++++------------ advanced-integration/views/checkout.ejs | 31 +-- 4 files changed, 149 insertions(+), 138 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..34d7e1a9 100644 --- a/advanced-integration/public/app.js +++ b/advanced-integration/public/app.js @@ -1,172 +1,179 @@ +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 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) { + // (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]; + 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 + > From 421ee2200f7041d55f802a687f933f6d728fc984 Mon Sep 17 00:00:00 2001 From: Greg Jopa <534034+gregjopa@users.noreply.github.com> Date: Thu, 17 Aug 2023 16:47:46 -0500 Subject: [PATCH 3/3] PR feedback - check for card decline use case --- advanced-integration/public/app.js | 23 ++++++++++++++++++----- standard-integration/index.html | 3 ++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/advanced-integration/public/app.js b/advanced-integration/public/app.js index 34d7e1a9..b26369c7 100644 --- a/advanced-integration/public/app.js +++ b/advanced-integration/public/app.js @@ -50,6 +50,9 @@ async function onApproveCallback(data, actions) { // (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'; @@ -62,15 +65,25 @@ async function onApproveCallback(data, 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) { + } else if ( + errorDetail || + !transaction || + transaction.status === 'DECLINED' + ) { // (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)); + 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'); - const transaction = orderData.purchase_units[0].payments.captures[0]; resultMessage( `Transaction ${transaction.status}: ${transaction.id}

See console for all available details`, ); diff --git a/standard-integration/index.html b/standard-integration/index.html index 8d657d3a..2bdc9a4b 100644 --- a/standard-integration/index.html +++ b/standard-integration/index.html @@ -86,7 +86,8 @@ // (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`, );