Skip to content

Add new card fields component #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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