diff --git a/.devcontainer/advanced-integration-v1/devcontainer.json b/.devcontainer/advanced-integration-v1/devcontainer.json new file mode 100644 index 00000000..b8f1d538 --- /dev/null +++ b/.devcontainer/advanced-integration-v1/devcontainer.json @@ -0,0 +1,40 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "PayPal Advanced Integration (v1)", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration/v1", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash ../../.devcontainer/advanced-integration-v1/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "npm install", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": { + "Start server": "npm start" + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [8888], + "portsAttributes": { + "8888": { + "label": "Preview of Advanced Checkout Flow", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "customizations": { + "vscode": { + "extensions": ["vsls-contrib.codetour","PayPal.vscode-paypal"], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + } +} diff --git a/.devcontainer/advanced-integration-v1/welcome-message.sh b/.devcontainer/advanced-integration-v1/welcome-message.sh new file mode 100644 index 00000000..ae9a72f9 --- /dev/null +++ b/.devcontainer/advanced-integration-v1/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +๐Ÿ‘‹ Welcome to the \"PayPal Advanced Checkout Integration Example\" + +๐Ÿ› ๏ธ Your environment is fully setup with all the required software. + +๐Ÿš€ Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +๐Ÿ‘‹ Welcome to the \"PayPal Advanced Checkout Integration Example\" + +๐Ÿ› ๏ธ Your environment is fully setup with all the required software. + +๐Ÿš€ The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/advanced-integration-v2/devcontainer.json b/.devcontainer/advanced-integration-v2/devcontainer.json new file mode 100644 index 00000000..9a4ab9ac --- /dev/null +++ b/.devcontainer/advanced-integration-v2/devcontainer.json @@ -0,0 +1,40 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "PayPal Advanced Integration (v2)", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration/v2", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash ../../.devcontainer/advanced-integration-v2/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "npm install", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": { + "Start server": "npm start" + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [8888], + "portsAttributes": { + "8888": { + "label": "Preview of Advanced Checkout Flow", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "customizations": { + "vscode": { + "extensions": ["vsls-contrib.codetour","PayPal.vscode-paypal"], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + } +} diff --git a/.devcontainer/advanced-integration-v2/welcome-message.sh b/.devcontainer/advanced-integration-v2/welcome-message.sh new file mode 100644 index 00000000..ae9a72f9 --- /dev/null +++ b/.devcontainer/advanced-integration-v2/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +๐Ÿ‘‹ Welcome to the \"PayPal Advanced Checkout Integration Example\" + +๐Ÿ› ๏ธ Your environment is fully setup with all the required software. + +๐Ÿš€ Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +๐Ÿ‘‹ Welcome to the \"PayPal Advanced Checkout Integration Example\" + +๐Ÿ› ๏ธ Your environment is fully setup with all the required software. + +๐Ÿš€ The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/save-payment-method/devcontainer.json b/.devcontainer/save-payment-method/devcontainer.json new file mode 100644 index 00000000..ec88eb6c --- /dev/null +++ b/.devcontainer/save-payment-method/devcontainer.json @@ -0,0 +1,40 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "PayPal Save Payment Method", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/save-payment-method", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash ../.devcontainer/save-payment-method/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "npm install", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": { + "Start server": "npm start" + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [8888], + "portsAttributes": { + "8888": { + "label": "Preview of Save Payment Method Flow", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "customizations": { + "vscode": { + "extensions": ["vsls-contrib.codetour","PayPal.vscode-paypal"], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + } +} \ No newline at end of file diff --git a/.devcontainer/save-payment-method/welcome-message.sh b/.devcontainer/save-payment-method/welcome-message.sh new file mode 100644 index 00000000..7ed0d59c --- /dev/null +++ b/.devcontainer/save-payment-method/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +๐Ÿ‘‹ Welcome to the \"PayPal Save Payment Method Integration Example\" + +๐Ÿ› ๏ธ Your environment is fully setup with all the required software. + +๐Ÿš€ Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +๐Ÿ‘‹ Welcome to the \"PayPal Save Payment Method Integration Example\" + +๐Ÿ› ๏ธ Your environment is fully setup with all the required software. + +๐Ÿš€ The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" \ No newline at end of file diff --git a/.devcontainer/standard-integration/devcontainer.json b/.devcontainer/standard-integration/devcontainer.json new file mode 100644 index 00000000..c6d0ffec --- /dev/null +++ b/.devcontainer/standard-integration/devcontainer.json @@ -0,0 +1,40 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "PayPal Standard Integration", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/standard-integration", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash ../.devcontainer/standard-integration/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "npm install", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": { + "Start server": "npm start" + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [8888], + "portsAttributes": { + "8888": { + "label": "Preview of Standard Checkout Flow", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "customizations": { + "vscode": { + "extensions": ["vsls-contrib.codetour","PayPal.vscode-paypal"], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + } +} diff --git a/.devcontainer/standard-integration/welcome-message.sh b/.devcontainer/standard-integration/welcome-message.sh new file mode 100644 index 00000000..78cce216 --- /dev/null +++ b/.devcontainer/standard-integration/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +๐Ÿ‘‹ Welcome to the \"PayPal Standard Checkout Integration Example\" + +๐Ÿ› ๏ธ Your environment is fully setup with all the required software. + +๐Ÿš€ Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +๐Ÿ‘‹ Welcome to the \"PayPal Standard Checkout Integration Example\" + +๐Ÿ› ๏ธ Your environment is fully setup with all the required software. + +๐Ÿš€ The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..ed606c67 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "env": { + "es2021": true + }, + "extends": ["eslint:recommended"], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": {} +} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 00000000..9ae8d9e3 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,35 @@ +name: validate +on: + # run on push but only for the main branch + push: + branches: + - main + # run for every pull request + pull_request: {} +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: ๐Ÿงน Check code formatting with Prettier + run: > + find . -name package.json -maxdepth 2 -type f | while read -r file; do + directory=$(dirname "$file") + cd "$directory" && npm run format:check && cd - + done + + - name: ๐Ÿ‘• Lint code with ESLint + run: > + find . -name package.json -maxdepth 3 -type f | while read -r file; do + directory=$(dirname "$file") + cd "$directory" && npm run lint && cd - + done diff --git a/LICENSE b/LICENSE index 261eeb9e..0699f06b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -175,24 +175,13 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] + Copyright 2022 PayPal Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/README.md b/README.md index e8bbac74..daf0636c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,64 @@ # PayPal Developer Docs Example Code -Examples from the official PayPal Developer Docs + +Examples from the official [PayPal Developer Docs](https://developer.paypal.com/). + +## Introduction and Overview + +This repository contains two directories: + +- [Standard integration](./standard-integration/) + - Set up standard payments on your checkout page for your buyers. +- [Advanced integration](./advanced-integration/) + - Build and customize a card payment form to accept debit and credit cards. + +**Not sure where to start?** Choose the [standard integration](./standard-integration/). + +### The PayPal JavaScript SDK + +These examples use the [PayPal JavaScript SDK](https://developer.paypal.com/sdk/js/) to display PayPal supported payment methods and provide a seamless checkout experience for your buyers. + +The SDK has several [configuration options](https://developer.paypal.com/sdk/js/configuration/) available. The examples in this repository provide the most minimal example possible to complete a successful transaction. + +## Know before you code + +### Setup a PayPal Account + +To get started with standard checkout, you'll need a developer, personal, or business account. + +[Sign Up](https://www.paypal.com/signin/client?flow=provisionUser) or [Log In](https://www.paypal.com/signin?returnUri=https%253A%252F%252Fdeveloper.paypal.com%252Fdeveloper%252Fapplications&intent=developer) + +You'll then need to visit the [Developer Dashboard](https://developer.paypal.com/dashboard/) to obtain credentials and to +make sandbox accounts. + +### Create an Application + +Once you've setup a PayPal account, you'll need to obtain a **Client ID** and **Secret**. [Create a sandbox application](https://developer.paypal.com/dashboard/applications/sandbox/create). + +### Have Node.js installed + +These examples will ask you to run commands like `npm install` and `npm start`. + +You'll need a version of node >= 16 which can be downloaded from the [Node.js website](https://nodejs.org/en/download/). + + +## PayPal Codespaces + +PayPal codespaces require a client ID and client secret for your app. + +### Link to codespaces + +| Application | Codespaces Link | +| ---- | ---- | +| Advanced Integration v2 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fadvanced-integration-v2%2Fdevcontainer.json)| +| Advanced Integration v1 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fadvanced-integration-v1%2Fdevcontainer.json)| +| Standard Integration | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fstandard-integration%2Fdevcontainer.json)| +| Save Payment Method Integration | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fsave-payment-method%2Fdevcontainer.json)| + +### Learn more + +You can read more about codespaces in the [PayPal Developer Docs](https://developer.paypal.com/api/rest/sandbox/codespaces). + +### Feedback + +* To report a bug or suggest a new feature, create an [issue in GitHub](https://github.com/paypal-examples/paypaldevsupport/issues/new/choose). +* To submit feedback, go to [PayPal Developer Docs](https://developer.paypal.com/api/rest/sandbox/codespaces) and select the "Feedback" tab. diff --git a/advanced-integration/.env b/advanced-integration/.env deleted file mode 100644 index f72af4c7..00000000 --- a/advanced-integration/.env +++ /dev/null @@ -1,2 +0,0 @@ -CLIENT_ID= -APP_SECRET= \ No newline at end of file diff --git a/advanced-integration/README.md b/advanced-integration/README.md index d282c7c3..ef1dd69d 100644 --- a/advanced-integration/README.md +++ b/advanced-integration/README.md @@ -1,9 +1,16 @@ -# Advanced Integration Example +# Advanced Checkout Integration Example + +This folder contains example code for a PayPal advanced Checkout integration using both the JavaScript SDK and Node.js to complete transactions with the PayPal REST API. + +- [`v2`](v2/README.md) contains sample code for the current advanced Checkout integration. This includes guidance on using Card Fields. +- [`v1`](v1/README.md) contains sample code for the legacy advanced Checkout integration. Use `v2` for new integrations. ## Instructions -1. Add `CLIENT_ID` and `APP_SECRET` to the `.env` file +These instructions apply to the sample code for both `v2` and `v1`: + +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 deleted file mode 100644 index cc028060..00000000 --- a/advanced-integration/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@paypalcorp/advanced-integration", - "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.0.0", - "ejs": "^3.1.6", - "express": "^4.17.3", - "node-fetch": "^3.2.1" - } -} diff --git a/advanced-integration/paypal-api.js b/advanced-integration/paypal-api.js deleted file mode 100644 index 082e2630..00000000 --- a/advanced-integration/paypal-api.js +++ /dev/null @@ -1,86 +0,0 @@ -import fetch from "node-fetch"; - -// set some important variables -const { CLIENT_ID, APP_SECRET } = process.env; -const base = "https://api-m.sandbox.paypal.com"; - -// call the create order method -export async function createOrder() { - const purchaseAmount = "100.00"; // TODO: pull prices from a database - const accessToken = await generateAccessToken(); - const url = `${base}/v2/checkout/orders`; - const response = await fetch(url, { - method: "post", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - intent: "CAPTURE", - purchase_units: [ - { - amount: { - currency_code: "USD", - value: purchaseAmount, - }, - }, - ], - }), - }); - - return handleResponse(response); -} - -// capture payment for an order -export async function capturePayment(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}`, - }, - }); - - return handleResponse(response); -} - -// generate access token -export async function generateAccessToken() { - const auth = Buffer.from(CLIENT_ID + ":" + APP_SECRET).toString("base64"); - const response = await fetch(`${base}/v1/oauth2/token`, { - method: "post", - body: "grant_type=client_credentials", - headers: { - Authorization: `Basic ${auth}`, - }, - }); - const jsonData = await handleResponse(response); - return jsonData.access_token; -} - -// generate client token -export async function generateClientToken() { - const accessToken = await generateAccessToken(); - const response = await fetch(`${base}/v1/identity/generate-token`, { - method: "post", - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Language": "en_US", - "Content-Type": "application/json", - }, - }); - console.log('response', response.status) - const jsonData = await handleResponse(response); - return jsonData.client_token; -} - -async function handleResponse(response) { - if (response.status === 200 || response.status === 201) { - return response.json(); - } - - const errorMessage = await response.text(); - throw new Error(errorMessage); -} diff --git a/advanced-integration/public/app.js b/advanced-integration/public/app.js deleted file mode 100644 index 056efc84..00000000 --- a/advanced-integration/public/app.js +++ /dev/null @@ -1,144 +0,0 @@ -paypal - .Buttons({ - // Sets up the transaction when a payment button is clicked - createOrder: function (data, actions) { - return fetch("/api/orders", { - method: "post", - // use the "body" param to optionally pass additional order information - // like product ids or amount - }) - .then((response) => response.json()) - .then((order) => order.id); - }, - // Finalize the transaction after payer approval - onApprove: function (data, actions) { - return fetch(`/api/orders/${data.orderID}/capture`, { - method: "post", - }) - .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'); - }); - }, - }) - .render("#paypal-button-container"); - -// 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", - // use the "body" param to optionally pass additional order information like - // product ids or amount. - }) - .then((res) => res.json()) - .then((orderData) => { - orderId = orderData.id; // needed later to complete capture - return orderData.id; - }); - }, - 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", - }, - }, - }).then((cardFields) => { - document.querySelector("#card-form").addEventListener("submit", (event) => { - event.preventDefault(); - cardFields - .submit({ - // Cardholder's first and last name - cardholderName: document.getElementById("card-holder-name").value, - // Billing Address - 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, - }, - }) - .then(() => { - fetch(`/api/orders/${orderId}/capture`, { - method: "post", - }) - .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!"); - }); - }) - .catch((err) => { - alert("Payment could not be captured! " + JSON.stringify(err)); - }); - }); - }); -} else { - // Hides card fields if the merchant isn't eligible - document.querySelector("#card-form").style = "display: none"; -} diff --git a/advanced-integration/server.js b/advanced-integration/server.js deleted file mode 100644 index e8069220..00000000 --- a/advanced-integration/server.js +++ /dev/null @@ -1,41 +0,0 @@ -import "dotenv/config"; -import express from "express"; -import * as paypal from "./paypal-api.js"; - -const app = express(); -app.set("view engine", "ejs"); -app.use(express.static("public")); - -// render checkout page with client id & unique client token -app.get("/", async (req, res) => { - const clientId = process.env.CLIENT_ID; - try { - const clientToken = await paypal.generateClientToken(); - res.render("checkout", { clientId, clientToken }); - } catch (err) { - res.status(500).send(err.message); - } -}); - -// create order -app.post("/api/orders", async (req, res) => { - try { - const order = await paypal.createOrder(); - res.json(order); - } catch (err) { - res.status(500).send(err.message); - } -}); - -// capture payment -app.post("/api/orders/:orderID/capture", async (req, res) => { - const { orderID } = req.params; - try { - const captureData = await paypal.capturePayment(orderID); - res.json(captureData); - } catch (err) { - res.status(500).send(err.message); - } -}); - -app.listen(8888); diff --git a/advanced-integration/v1/.gitignore b/advanced-integration/v1/.gitignore new file mode 100644 index 00000000..4c49bd78 --- /dev/null +++ b/advanced-integration/v1/.gitignore @@ -0,0 +1 @@ +.env diff --git a/advanced-integration/v1/README.md b/advanced-integration/v1/README.md new file mode 100644 index 00000000..152ef9ae --- /dev/null +++ b/advanced-integration/v1/README.md @@ -0,0 +1,14 @@ +# Advanced Integration Example + +This folder contains example code for [version 1](https://developer.paypal.com/docs/checkout/advanced/integrate/sdk/v1) of a PayPal advanced Checkout integration using the JavaScript SDK and Node.js to complete transactions with the PayPal REST API. + +> **Note:** Version 1 is a legacy integration. Use [version 2](https://developer.paypal.com/docs/checkout/advanced/integrate/) for new integrations. + +## 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. Run `npm install`. +4. Run `npm start`. +5. Open http://localhost:8888. +6. 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/v1/client/app.js b/advanced-integration/v1/client/app.js new file mode 100644 index 00000000..65f048d7 --- /dev/null +++ b/advanced-integration/v1/client/app.js @@ -0,0 +1,186 @@ +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 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); + } 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), + ); + } + } 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", + }, + }, + fields: { + number: { + selector: "#card-number", + placeholder: "4111 1111 1111 1111", + }, + cvv: { + selector: "#cvv", + placeholder: "123", + }, + expirationDate: { + selector: "#expiration-date", + placeholder: "MM/YY", + }, + }, + }).then((cardFields) => { + document.querySelector("#card-form").addEventListener("submit", (event) => { + event.preventDefault(); + cardFields + .submit({ + // Cardholder's first and last name + cardholderName: document.getElementById("card-holder-name").value, + // Billing Address + 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, + }, + }) + .then((data) => { + return onApproveCallback(data); + }) + .catch((orderData) => { + resultMessage( + `Sorry, your transaction could not be processed...

${JSON.stringify( + orderData, + )}`, + ); + }); + }); + }); +} else { + // Hides card fields if the merchant isn't eligible + document.querySelector("#card-form").style = "display: none"; +} diff --git a/advanced-integration/v1/env.example b/advanced-integration/v1/env.example new file mode 100644 index 00000000..2251fbbb --- /dev/null +++ b/advanced-integration/v1/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/advanced-integration/v1/package.json b/advanced-integration/v1/package.json new file mode 100644 index 00000000..0e93024f --- /dev/null +++ b/advanced-integration/v1/package.json @@ -0,0 +1,24 @@ +{ + "name": "paypal-advanced-integration", + "description": "Sample Node.js web app to integrate PayPal Advanced Checkout 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 client/*.js --no-config-lookup" + }, + "license": "Apache-2.0", + "dependencies": { + "dotenv": "^16.3.1", + "ejs": "^3.1.9", + "express": "^4.18.2", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/advanced-integration/v1/server/server.js b/advanced-integration/v1/server/server.js new file mode 100644 index 00000000..a7d84407 --- /dev/null +++ b/advanced-integration/v1/server/server.js @@ -0,0 +1,178 @@ +import express from "express"; +import fetch from "node-fetch"; +import "dotenv/config"; + +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"); +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 generateAccessToken = async () => { + 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"); + const response = await fetch(`${base}/v1/oauth2/token`, { + method: "POST", + body: "grant_type=client_credentials", + headers: { + Authorization: `Basic ${auth}`, + }, + }); + + const data = await response.json(); + return data.access_token; + } catch (error) { + console.error("Failed to generate Access Token:", error); + } +}; + +/** + * Generate a client token for rendering the hosted card fields. + * @see https://developer.paypal.com/docs/checkout/advanced/integrate/#link-integratebackend + */ +const generateClientToken = async () => { + const accessToken = await generateAccessToken(); + const url = `${base}/v1/identity/generate-token`; + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Language": "en_US", + "Content-Type": "application/json", + }, + }); + + return handleResponse(response); +}; + +/** + * 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(); + const url = `${base}/v2/checkout/orders`; + const payload = { + intent: "CAPTURE", + purchase_units: [ + { + amount: { + currency_code: "USD", + value: "100.00", + }, + }, + ], + }; + + 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); + } +} + +// render checkout page with client id & unique client token +app.get("/", async (req, res) => { + try { + const { jsonResponse } = await generateClientToken(); + res.render("checkout", { + clientId: PAYPAL_CLIENT_ID, + clientToken: jsonResponse.client_token, + }); + } catch (err) { + res.status(500).send(err.message); + } +}); + +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." }); + } +}); + +app.listen(PORT, () => { + console.log(`Node server listening at http://localhost:${PORT}/`); +}); diff --git a/advanced-integration/v1/server/views/checkout.ejs b/advanced-integration/v1/server/views/checkout.ejs new file mode 100644 index 00000000..85cd7085 --- /dev/null +++ b/advanced-integration/v1/server/views/checkout.ejs @@ -0,0 +1,104 @@ + + + + + + PayPal JS SDK Advanced Integration + + + + +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+

+ +
+

+
+ + + diff --git a/advanced-integration/views/checkout.ejs b/advanced-integration/v1/views/checkout.ejs similarity index 100% rename from advanced-integration/views/checkout.ejs rename to advanced-integration/v1/views/checkout.ejs diff --git a/advanced-integration/v2/.env.example b/advanced-integration/v2/.env.example new file mode 100644 index 00000000..2251fbbb --- /dev/null +++ b/advanced-integration/v2/.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/advanced-integration/v2/.gitignore b/advanced-integration/v2/.gitignore new file mode 100644 index 00000000..4c49bd78 --- /dev/null +++ b/advanced-integration/v2/.gitignore @@ -0,0 +1 @@ +.env diff --git a/advanced-integration/v2/README.md b/advanced-integration/v2/README.md new file mode 100644 index 00000000..332d0d88 --- /dev/null +++ b/advanced-integration/v2/README.md @@ -0,0 +1,14 @@ +# Advanced Integration Example + +This folder contains example code for [version 2](https://developer.paypal.com/docs/checkout/advanced/integrate/) of a PayPal advanced Checkout integration using the JavaScript SDK and Node.js to complete transactions with the PayPal REST API. + +Version 2 is the current advanced Checkout integration, and includes Card Fields. + +## 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. Run `npm install` +4. Run `npm start` +5. Open http://localhost:8888 +6. 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/v2/client/checkout.js b/advanced-integration/v2/client/checkout.js new file mode 100644 index 00000000..29d9310c --- /dev/null +++ b/advanced-integration/v2/client/checkout.js @@ -0,0 +1,166 @@ +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 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); + } 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), + ); + } + } 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"); + +const cardField = window.paypal.CardFields({ + createOrder: createOrderCallback, + onApprove: onApproveCallback, +}); + +// Render each field after checking for eligibility +if (cardField.isEligible()) { + const nameField = cardField.NameField(); + nameField.render("#card-name-field-container"); + + const numberField = cardField.NumberField(); + numberField.render("#card-number-field-container"); + + const cvvField = cardField.CVVField(); + cvvField.render("#card-cvv-field-container"); + + const expiryField = cardField.ExpiryField(); + expiryField.render("#card-expiry-field-container"); + + // Add click listener to submit button and call the submit function on the CardField component + document + .getElementById("card-field-submit-button") + .addEventListener("click", () => { + cardField + .submit({ + // From your billing address fields + billingAddress: { + addressLine1: document.getElementById("card-billing-address-line-1") + .value, + addressLine2: document.getElementById("card-billing-address-line-2") + .value, + adminArea1: document.getElementById( + "card-billing-address-admin-area-line-1", + ).value, + adminArea2: document.getElementById( + "card-billing-address-admin-area-line-2", + ).value, + countryCode: document.getElementById( + "card-billing-address-country-code", + ).value, + postalCode: document.getElementById( + "card-billing-address-postal-code", + ).value, + }, + }) + .catch((error) => { + resultMessage( + `Sorry, your transaction could not be processed...

${error}`, + ); + }); + }); +} else { + // Hides card fields if the merchant isn't eligible + document.querySelector("#card-form").style = "display: none"; +} + +// 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/advanced-integration/v2/package.json b/advanced-integration/v2/package.json new file mode 100644 index 00000000..0e93024f --- /dev/null +++ b/advanced-integration/v2/package.json @@ -0,0 +1,24 @@ +{ + "name": "paypal-advanced-integration", + "description": "Sample Node.js web app to integrate PayPal Advanced Checkout 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 client/*.js --no-config-lookup" + }, + "license": "Apache-2.0", + "dependencies": { + "dotenv": "^16.3.1", + "ejs": "^3.1.9", + "express": "^4.18.2", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/advanced-integration/v2/server/server.js b/advanced-integration/v2/server/server.js new file mode 100644 index 00000000..f1188758 --- /dev/null +++ b/advanced-integration/v2/server/server.js @@ -0,0 +1,160 @@ +import express from "express"; +import fetch from "node-fetch"; +import "dotenv/config"; + +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")); + +// 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 generateAccessToken = async () => { + 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"); + const response = await fetch(`${base}/v1/oauth2/token`, { + method: "POST", + body: "grant_type=client_credentials", + headers: { + Authorization: `Basic ${auth}`, + }, + }); + + const data = await response.json(); + return data.access_token; + } catch (error) { + console.error("Failed to generate Access Token:", error); + } +}; + +/** + * 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(); + const url = `${base}/v2/checkout/orders`; + const payload = { + intent: "CAPTURE", + purchase_units: [ + { + amount: { + currency_code: "USD", + value: "100.00", + }, + }, + ], + }; + + 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.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." }); + } +}); + +// render checkout page with client id & unique client token +app.get("/", async (req, res) => { + try { + res.render("checkout", { + clientId: PAYPAL_CLIENT_ID, + }); + } catch (err) { + res.status(500).send(err.message); + } +}); + +app.listen(PORT, () => { + console.log(`Node server listening at http://localhost:${PORT}/`); +}); diff --git a/advanced-integration/v2/server/views/checkout.ejs b/advanced-integration/v2/server/views/checkout.ejs new file mode 100644 index 00000000..5d129f60 --- /dev/null +++ b/advanced-integration/v2/server/views/checkout.ejs @@ -0,0 +1,51 @@ + + + + + + + PayPal JS SDK Advanced Integration - Checkout Flow + + +
+ +
+
+
+
+
+ +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+

+ +
+

+ + + + \ No newline at end of file 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..4c49bd78 --- /dev/null +++ b/save-payment-method/.gitignore @@ -0,0 +1 @@ +.env diff --git a/save-payment-method/README.md b/save-payment-method/README.md new file mode 100644 index 00000000..f1f4a0e9 --- /dev/null +++ b/save-payment-method/README.md @@ -0,0 +1,15 @@ +# 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. + +[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) +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..eebd2017 --- /dev/null +++ b/save-payment-method/client/app.js @@ -0,0 +1,97 @@ +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.
+ See the return buyer experience + `, + ); + + 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/package.json b/save-payment-method/package.json new file mode 100644 index 00000000..d1596b09 --- /dev/null +++ b/save-payment-method/package.json @@ -0,0 +1,24 @@ +{ + "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 client/*.js --no-config-lookup" + }, + "license": "Apache-2.0", + "dependencies": { + "dotenv": "^16.3.1", + "ejs": "^3.1.9", + "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..7d2596f2 --- /dev/null +++ b/save-payment-method/server/server.js @@ -0,0 +1,193 @@ +import express from "express"; +import fetch from "node-fetch"; +import "dotenv/config"; + +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")); + +// 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 (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"); + } + const auth = Buffer.from( + PAYPAL_CLIENT_ID + ":" + PAYPAL_CLIENT_SECRET, + ).toString("base64"); + + const response = await fetch(`${base}/v1/oauth2/token`, { + method: "POST", + body: urlEncodedParams, + 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(); + 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", + shipping_preference: "NO_SHIPPING", + }, + }, + }, + }; + + 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.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); + console.log("capture response", jsonResponse); + res.status(httpStatusCode).json(jsonResponse); + } catch (error) { + console.error("Failed to create order:", error); + res.status(500).json({ error: "Failed to capture order." }); + } +}); + +// render checkout page with client id & user id token +app.get("/", async (req, res) => { + try { + const { jsonResponse } = await authenticate({ + target_customer_id: 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, () => { + console.log(`Node server listening at http://localhost:${PORT}/`); +}); diff --git a/save-payment-method/server/views/checkout.ejs b/save-payment-method/server/views/checkout.ejs new file mode 100644 index 00000000..69cd3e78 --- /dev/null +++ b/save-payment-method/server/views/checkout.ejs @@ -0,0 +1,17 @@ + + + + + + PayPal JS SDK Save Payment Method Integration + + +
+

+ + + + diff --git a/standard-integration/.env b/standard-integration/.env deleted file mode 100644 index f72af4c7..00000000 --- a/standard-integration/.env +++ /dev/null @@ -1,2 +0,0 @@ -CLIENT_ID= -APP_SECRET= \ No newline at end of file diff --git a/standard-integration/.env.example b/standard-integration/.env.example new file mode 100644 index 00000000..2251fbbb --- /dev/null +++ b/standard-integration/.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/standard-integration/.gitignore b/standard-integration/.gitignore new file mode 100644 index 00000000..4c49bd78 --- /dev/null +++ b/standard-integration/.gitignore @@ -0,0 +1 @@ +.env diff --git a/standard-integration/README.md b/standard-integration/README.md index 7220aea8..408396b8 100644 --- a/standard-integration/README.md +++ b/standard-integration/README.md @@ -1,13 +1,13 @@ # 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. Add your app's `CLIENT_ID` and `APP_SECRET` to the `.env` file -2. Replace `test` in `public/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 [client/checkout.html](client/checkout.html) 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). +7. Click "PayPal" and log in with one of your [Sandbox test accounts](https://developer.paypal.com/dashboard/accounts) diff --git a/standard-integration/client/Luxdesignpay b/standard-integration/client/Luxdesignpay new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/standard-integration/client/Luxdesignpay @@ -0,0 +1 @@ + diff --git a/standard-integration/client/app.js b/standard-integration/client/app.js new file mode 100644 index 00000000..cab942a0 --- /dev/null +++ b/standard-integration/client/app.js @@ -0,0 +1,94 @@ +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`, + ); + 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/standard-integration/client/checkout.html b/standard-integration/client/checkout.html new file mode 100644 index 00000000..7b959c2f --- /dev/null +++ b/standard-integration/client/checkout.html @@ -0,0 +1,15 @@ + + + + + + PayPal JS SDK Standard Integration + + +
+

+ + + + + diff --git a/standard-integration/package.json b/standard-integration/package.json index 766860ec..f00bfb37 100644 --- a/standard-integration/package.json +++ b/standard-integration/package.json @@ -1,18 +1,23 @@ { - "name": "@paypalcorp/standard-integration", + "name": "paypal-standard-integration", + "description": "Sample Node.js web app to integrate PayPal Standard Checkout for online payments", "version": "1.0.0", - "main": "paypal-api.js", + "main": "server/server.js", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node server.js" + "start": "nodemon server/server.js", + "format": "npx prettier --write **/*.{js,md}", + "format:check": "npx prettier --check **/*.{js,md}", + "lint": "npx eslint server/*.js client/*.js --no-config-lookup" }, - "author": "", "license": "Apache-2.0", - "description": "", "dependencies": { - "dotenv": "^16.0.0", - "express": "^4.17.3", - "node-fetch": "^3.2.1" + "dotenv": "^16.3.1", + "express": "^4.18.2", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" } } diff --git a/standard-integration/paypal-api.js b/standard-integration/paypal-api.js deleted file mode 100644 index 35cca914..00000000 --- a/standard-integration/paypal-api.js +++ /dev/null @@ -1,66 +0,0 @@ -import fetch from "node-fetch"; - -const { CLIENT_ID, APP_SECRET } = process.env; -const base = "https://api-m.sandbox.paypal.com"; - -export async function createOrder() { - const accessToken = await generateAccessToken(); - const url = `${base}/v2/checkout/orders`; - const response = await fetch(url, { - method: "post", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - intent: "CAPTURE", - purchase_units: [ - { - amount: { - currency_code: "USD", - value: "100.00", - }, - }, - ], - }), - }); - - return handleResponse(response); -} - -export async function capturePayment(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}`, - }, - }); - - return handleResponse(response); -} - -export async function generateAccessToken() { - const auth = Buffer.from(CLIENT_ID + ":" + APP_SECRET).toString("base64"); - const response = await fetch(`${base}/v1/oauth2/token`, { - method: "post", - body: "grant_type=client_credentials", - headers: { - Authorization: `Basic ${auth}`, - }, - }); - - const jsonData = await handleResponse(response); - return jsonData.access_token; -} - -async function handleResponse(response) { - if (response.status === 200 || response.status === 201) { - return response.json(); - } - - const errorMessage = await response.text(); - throw new Error(errorMessage); -} diff --git a/standard-integration/public/index.html b/standard-integration/public/index.html deleted file mode 100644 index f0f8b659..00000000 --- a/standard-integration/public/index.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - -
- - - diff --git a/standard-integration/server.js b/standard-integration/server.js deleted file mode 100644 index d9d40db4..00000000 --- a/standard-integration/server.js +++ /dev/null @@ -1,28 +0,0 @@ -import "dotenv/config"; // loads variables from .env file -import express from "express"; -import * as paypal from "./paypal-api.js"; - -const app = express(); - -app.use(express.static("public")); - -app.post("/api/orders", async (req, res) => { - try { - const order = await paypal.createOrder(); - res.json(order); - } catch (err) { - res.status(500).send(err.message); - } -}); - -app.post("/api/orders/:orderID/capture", async (req, res) => { - const { orderID } = req.params; - try { - const captureData = await paypal.capturePayment(orderID); - res.json(captureData); - } catch (err) { - res.status(500).send(err.message); - } -}); - -app.listen(8888); diff --git a/standard-integration/server/server.js b/standard-integration/server/server.js new file mode 100644 index 00000000..fba19829 --- /dev/null +++ b/standard-integration/server/server.js @@ -0,0 +1,152 @@ +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 generateAccessToken = async () => { + 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"); + const response = await fetch(`${base}/v1/oauth2/token`, { + method: "POST", + body: "grant_type=client_credentials", + headers: { + Authorization: `Basic ${auth}`, + }, + }); + + const data = await response.json(); + return data.access_token; + } catch (error) { + console.error("Failed to generate Access Token:", error); + } +}; + +/** + * 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(); + const url = `${base}/v2/checkout/orders`; + const payload = { + intent: "CAPTURE", + purchase_units: [ + { + amount: { + currency_code: "USD", + value: "110.00", + }, + }, + ], + }; + + 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.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}/`); +});