diff --git a/README.md b/README.md index 6c7a81c..ecdba48 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ ![PayPal Developer Cover](https://github.com/paypaldev/.github/blob/main/pp-cover.png) +
Twitter: PayPal Developer @@ -18,17 +19,18 @@
# PayPal JavaScript FullStack Advanced Checkout + This sample app shows how to build and customize a card payment form to accept debit and credit cards. Please make sure to style the card form so that it aligns with your business branding. To create this application from scratch, follow the [Advanced Checkout integration](https://developer.paypal.com/docs/checkout/advanced/integrate) guide from the [PayPal Developer](https://developer.paypal.com/home) docs. - ## Run this project ### PayPal Codespaces + [![Open Code In GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypaldev/PayPal-JavaScript-FullStack-Advanced-Checkout-Sample?devcontainer_path=.devcontainer%2Fdevcontainer.json) -- Rename the ``.env.example`` file to `.env`. +- Rename the `.env.example` file to `.env`. - Add your environment variables in the `.env` file. ### Locally @@ -37,6 +39,7 @@ To create this application from scratch, follow the [Advanced Checkout integrati - Add your environment variables in the `.env` file. Complete the steps in [Get started](https://developer.paypal.com/api/rest/) to get the following sandbox account information from the Developer Dashboard: + - Sandbox client ID and the secret of [a REST app](https://www.paypal.com/signin?returnUri=https%3A%2F%2Fdeveloper.paypal.com%2Fdeveloper%2Fapplications&_ga=1.252581760.841672670.1664266268). - Access token to use the PayPal REST API server. @@ -59,8 +62,9 @@ Expiration Date: `01/2025` CVV: `123` ## PayPal Developer Community + The PayPal Developer community helps you build your career while improving your products and the developer experience. You’ll be able to contribute code and documentation, meet new people and learn from the open-source community. - -* Website: [developer.paypal.com](https://developer.paypal.com) -* Twitter: [@paypaldev](https://twitter.com/paypaldev) -* GitHub: [@paypal](https://github.com/paypal) + +- Website: [developer.paypal.com](https://developer.paypal.com) +- Twitter: [@paypaldev](https://twitter.com/paypaldev) +- GitHub: [@paypal](https://github.com/paypal) diff --git a/client/app.js b/client/app.js index 21fee39..a069fe1 100644 --- a/client/app.js +++ b/client/app.js @@ -1,186 +1,188 @@ 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}`); + resultMessage(""); + 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 transaction = - orderData?.purchase_units?.[0]?.payments?.captures?.[0] || - orderData?.purchase_units?.[0]?.payments?.authorizations?.[0]; - const errorDetail = orderData?.details?.[0]; - - // this actions.restart() behavior only applies to the Buttons component - if (errorDetail?.issue === "INSTRUMENT_DECLINED" && !data.card && 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 || - !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); +} + +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]; + + // this actions.restart() behavior only applies to the Buttons component + if (errorDetail?.issue === "INSTRUMENT_DECLINED" && !data.card && 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 || + !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 { - // (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}

See console for all available details`, - ); - console.log( - "Capture result", - orderData, - JSON.stringify(orderData, null, 2), - ); + errorMessage = JSON.stringify(orderData); } - } catch (error) { - console.error(error); + + 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( - `Sorry, your transaction could not be processed...

${error}`, + `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}`, + ); } - - window.paypal - .Buttons({ - createOrder: createOrderCallback, - onApprove: onApproveCallback, - }) - .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 (window.paypal.HostedFields.isEligible()) { - // Renders card fields - window.paypal.HostedFields.render({ - // Call your server to set up the transaction - createOrder: createOrderCallback, - styles: { - ".valid": { - color: "green", - }, - ".invalid": { - color: "red", - }, +} + +window.paypal + .Buttons({ + createOrder: createOrderCallback, + onApprove: onApproveCallback, + }) + .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 (window.paypal.HostedFields.isEligible()) { + // Renders card fields + window.paypal.HostedFields.render({ + // Call your server to set up the transaction + createOrder: createOrderCallback, + styles: { + ".valid": { + color: "green", + }, + ".invalid": { + color: "red", }, - fields: { - number: { - selector: "#card-number", - placeholder: "4111 1111 1111 1111", - }, - cvv: { - selector: "#cvv", - placeholder: "123", - }, - expirationDate: { - selector: "#expiration-date", - placeholder: "MM/YY", - }, + }, + fields: { + number: { + selector: "#card-number", + placeholder: "4111 1111 1111 1111", }, - }).then((cardFields) => { - document.querySelector("#card-form").addEventListener("submit", (event) => { + cvv: { + selector: "#cvv", + placeholder: "123", + }, + expirationDate: { + selector: "#expiration-date", + placeholder: "MM/YY", + }, + }, + }).then((cardFields) => { + document + .querySelector("#card-form") + .addEventListener("submit", async (event) => { event.preventDefault(); - cardFields - .submit({ - // Cardholder's first and last name - cardholderName: document.getElementById("card-holder-name").value, - // Billing Address + try { + const { value: cardHolderName } = + document.getElementById("card-holder-name"); + const { value: streetAddress } = document.getElementById( + "card-billing-address-street", + ); + const { value: extendedAddress } = document.getElementById( + "card-billing-address-unit", + ); + const { value: region } = document.getElementById( + "card-billing-address-state", + ); + const { value: locality } = document.getElementById( + "card-billing-address-city", + ); + const { value: postalCode } = document.getElementById( + "card-billing-address-zip", + ); + const { value: countryCodeAlpha2 } = document.getElementById( + "card-billing-address-country", + ); + + const data = await cardFields.submit({ + cardHolderName, billingAddress: { - // Street address, line 1 - streetAddress: document.getElementById( - "card-billing-address-street", - ).value, - // Street address, line 2 (Ex: Unit, Apartment, etc.) - extendedAddress: document.getElementById( - "card-billing-address-unit", - ).value, - // State - region: document.getElementById("card-billing-address-state").value, - // City - locality: document.getElementById("card-billing-address-city") - .value, - // Postal Code - postalCode: document.getElementById("card-billing-address-zip") - .value, - // Country Code - countryCodeAlpha2: document.getElementById( - "card-billing-address-country", - ).value, + streetAddress, + extendedAddress, + region, + locality, + postalCode, + countryCodeAlpha2, }, - }) - .then((data) => { - return onApproveCallback(data); - }) - .catch((orderData) => { - resultMessage( - `Sorry, your transaction could not be processed...

${JSON.stringify( - orderData, - )}`, - ); }); + return await onApproveCallback(data); + } catch (error) { + alert("Payment could not be captured! " + JSON.stringify(error)); + } }); - }); - } else { - // Hides card fields if the merchant isn't eligible - document.querySelector("#card-form").style = "display: none"; - } \ No newline at end of file + }); +} else { + // Hides card fields if the merchant isn't eligible + document.querySelector("#card-form").style = "display: none"; +} diff --git a/package.json b/package.json index 87a32dd..0f3a64f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "paypal-advanced-integration", + "name": "paypal-js-advanced-integration-ib", "description": "Sample Node.js web app to integrate PayPal Advanced Checkout for online payments", "version": "1.0.0", "main": "server/server.js", diff --git a/server/server.js b/server/server.js index 79c1601..f72bfee 100644 --- a/server/server.js +++ b/server/server.js @@ -176,4 +176,4 @@ app.post("/api/orders/:orderID/capture", async (req, res) => { app.listen(PORT, () => { console.log(`Node server listening at http://localhost:${PORT}/`); -}); \ No newline at end of file +});