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}