Skip to content

Commit

Permalink
Add new card fields beta component (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
gregjopa authored Oct 5, 2023
1 parent d3c1ada commit 5c89c3c
Show file tree
Hide file tree
Showing 8 changed files with 422 additions and 0 deletions.
40 changes: 40 additions & 0 deletions .devcontainer/advanced-integration-beta/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// For more details, see https://aka.ms/devcontainer.json.
{
"name": "PayPal Advanced Integration (beta)",
"image": "mcr.microsoft.com/devcontainers/javascript-node:20",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration/beta",
// Use 'onCreateCommand' to run commands when creating the container.
"onCreateCommand": "bash ../.devcontainer/advanced-integration-beta/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"],
"settings": {
"git.openRepositoryInParentFolders": "always"
}
}
}
}
23 changes: 23 additions & 0 deletions .devcontainer/advanced-integration-beta/welcome-message.sh
Original file line number Diff line number Diff line change
@@ -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"
5 changes: 5 additions & 0 deletions advanced-integration/beta/.env.example
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions advanced-integration/beta/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Advanced Integration Example

This folder contains example code for an Advanced PayPal integration using both the JS SDK and Node.js to complete transactions with the PayPal REST API.

## Instructions

1. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET`.
2. Run `npm install`
3. Run `npm start`
4. Open http://localhost:8888
5. Enter the credit card number provided from one of your [sandbox accounts](https://developer.paypal.com/dashboard/accounts) or [generate a new credit card](https://developer.paypal.com/dashboard/creditCardGenerator)
24 changes: 24 additions & 0 deletions advanced-integration/beta/client/checkout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css"
href="https://www.paypalobjects.com/webstatic/en_US/developer/docs/css/cardfields.css" />
<title>PayPal JS SDK Advanced Integration - Checkout Flow</title>
</head>
<body>
<div id="paypal-button-container"></div>
<div id="card-form">
<div id="card-name-field-container"></div>
<div id="card-number-field-container"></div>
<div id="card-expiry-field-container"></div>
<div id="card-cvv-field-container"></div>
<button id="multi-card-field-button" type="button">Pay now with Card</button>
</div>
<p id="result-message"></p>
<!-- Replace the "test" client-id value with your client-id -->
<script src="https://www.paypal.com/sdk/js?components=buttons,card-fields&client-id=test"></script>
<script src="checkout.js"></script>
</body>
</html>
144 changes: 144 additions & 0 deletions advanced-integration/beta/client/checkout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
async function createOrderCallback() {
try {
const response = await fetch("/api/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
// use the "body" param to optionally pass additional order information
// like product ids and quantities
body: JSON.stringify({
cart: [
{
id: "YOUR_PRODUCT_ID",
quantity: "YOUR_PRODUCT_QUANTITY",
},
],
}),
});

const orderData = await response.json();

if (orderData.id) {
return orderData.id;
} else {
const errorDetail = orderData?.details?.[0];
const errorMessage = errorDetail
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
: JSON.stringify(orderData);

throw new Error(errorMessage);
}
} catch (error) {
console.error(error);
resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
}
}

async function onApproveCallback(data, actions) {
try {
const response = await fetch(`/api/orders/${data.orderID}/capture`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});

const orderData = await response.json();
// Three cases to handle:
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// (2) Other non-recoverable errors -> Show a failure message
// (3) Successful transaction -> Show confirmation or thank you message

const transaction =
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
const errorDetail = orderData?.details?.[0];

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

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("multi-card-field-button")
.addEventListener("click", () => {
cardField.submit().catch((error) => {
resultMessage(
`Sorry, your transaction could not be processed...<br><br>${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;
}
23 changes: 23 additions & 0 deletions advanced-integration/beta/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"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 --env=node && npx eslint client/*.js --env=browser"
},
"license": "Apache-2.0",
"dependencies": {
"dotenv": "^16.3.1",
"express": "^4.18.2",
"node-fetch": "^3.3.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
Loading

0 comments on commit 5c89c3c

Please sign in to comment.