-
Notifications
You must be signed in to change notification settings - Fork 506
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Align standard and advanced integrations (#66)
* Minor tweaks to standard integration * Share JS code between card fields and buttons * PR feedback - check for card decline use case
- Loading branch information
Showing
7 changed files
with
171 additions
and
149 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,172 +1,192 @@ | ||
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...<br><br>${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]; | ||
|
||
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 || | ||
!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}<br><br>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...<br><br>${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: "<YOUR_PRODUCT_STOCK_KEEPING_UNIT>", | ||
quantity: "<YOUR_PRODUCT_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 = '<h3>Thank you for your payment!</h3>'; | ||
// 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: "<YOUR_PRODUCT_STOCK_KEEPING_UNIT>", | ||
quantity: "<YOUR_PRODUCT_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...<br><br>${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'; | ||
} |
Oops, something went wrong.