Skip to content

Commit 5c89c3c

Browse files
authored
Add new card fields beta component (#84)
1 parent d3c1ada commit 5c89c3c

File tree

8 files changed

+422
-0
lines changed

8 files changed

+422
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// For more details, see https://aka.ms/devcontainer.json.
2+
{
3+
"name": "PayPal Advanced Integration (beta)",
4+
"image": "mcr.microsoft.com/devcontainers/javascript-node:20",
5+
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration/beta",
6+
// Use 'onCreateCommand' to run commands when creating the container.
7+
"onCreateCommand": "bash ../.devcontainer/advanced-integration-beta/welcome-message.sh",
8+
// Use 'postCreateCommand' to run commands after the container is created.
9+
"postCreateCommand": "npm install",
10+
// Use 'postAttachCommand' to run commands when attaching to the container.
11+
"postAttachCommand": {
12+
"Start server": "npm start"
13+
},
14+
// Use 'forwardPorts' to make a list of ports inside the container available locally.
15+
"forwardPorts": [8888],
16+
"portsAttributes": {
17+
"8888": {
18+
"label": "Preview of Advanced Checkout Flow",
19+
"onAutoForward": "openBrowserOnce"
20+
}
21+
},
22+
"secrets": {
23+
"PAYPAL_CLIENT_ID": {
24+
"description": "Sandbox client ID of the application.",
25+
"documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox"
26+
},
27+
"PAYPAL_CLIENT_SECRET": {
28+
"description": "Sandbox secret of the application.",
29+
"documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox"
30+
}
31+
},
32+
"customizations": {
33+
"vscode": {
34+
"extensions": ["vsls-contrib.codetour"],
35+
"settings": {
36+
"git.openRepositoryInParentFolders": "always"
37+
}
38+
}
39+
}
40+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/sh
2+
3+
set -e
4+
5+
WELCOME_MESSAGE="
6+
👋 Welcome to the \"PayPal Advanced Checkout Integration Example\"
7+
8+
🛠️ Your environment is fully setup with all the required software.
9+
10+
🚀 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."
11+
12+
ALTERNATE_WELCOME_MESSAGE="
13+
👋 Welcome to the \"PayPal Advanced Checkout Integration Example\"
14+
15+
🛠️ Your environment is fully setup with all the required software.
16+
17+
🚀 The checkout page will automatically open in the browser after the server is started."
18+
19+
if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then
20+
WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}"
21+
fi
22+
23+
sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Create an application to obtain credentials at
2+
# https://developer.paypal.com/dashboard/applications/sandbox
3+
4+
PAYPAL_CLIENT_ID=YOUR_CLIENT_ID_GOES_HERE
5+
PAYPAL_CLIENT_SECRET=YOUR_SECRET_GOES_HERE
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Advanced Integration Example
2+
3+
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.
4+
5+
## Instructions
6+
7+
1. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET`.
8+
2. Run `npm install`
9+
3. Run `npm start`
10+
4. Open http://localhost:8888
11+
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)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<link rel="stylesheet" type="text/css"
7+
href="https://www.paypalobjects.com/webstatic/en_US/developer/docs/css/cardfields.css" />
8+
<title>PayPal JS SDK Advanced Integration - Checkout Flow</title>
9+
</head>
10+
<body>
11+
<div id="paypal-button-container"></div>
12+
<div id="card-form">
13+
<div id="card-name-field-container"></div>
14+
<div id="card-number-field-container"></div>
15+
<div id="card-expiry-field-container"></div>
16+
<div id="card-cvv-field-container"></div>
17+
<button id="multi-card-field-button" type="button">Pay now with Card</button>
18+
</div>
19+
<p id="result-message"></p>
20+
<!-- Replace the "test" client-id value with your client-id -->
21+
<script src="https://www.paypal.com/sdk/js?components=buttons,card-fields&client-id=test"></script>
22+
<script src="checkout.js"></script>
23+
</body>
24+
</html>
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
async function createOrderCallback() {
2+
try {
3+
const response = await fetch("/api/orders", {
4+
method: "POST",
5+
headers: {
6+
"Content-Type": "application/json",
7+
},
8+
// use the "body" param to optionally pass additional order information
9+
// like product ids and quantities
10+
body: JSON.stringify({
11+
cart: [
12+
{
13+
id: "YOUR_PRODUCT_ID",
14+
quantity: "YOUR_PRODUCT_QUANTITY",
15+
},
16+
],
17+
}),
18+
});
19+
20+
const orderData = await response.json();
21+
22+
if (orderData.id) {
23+
return orderData.id;
24+
} else {
25+
const errorDetail = orderData?.details?.[0];
26+
const errorMessage = errorDetail
27+
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
28+
: JSON.stringify(orderData);
29+
30+
throw new Error(errorMessage);
31+
}
32+
} catch (error) {
33+
console.error(error);
34+
resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
35+
}
36+
}
37+
38+
async function onApproveCallback(data, actions) {
39+
try {
40+
const response = await fetch(`/api/orders/${data.orderID}/capture`, {
41+
method: "POST",
42+
headers: {
43+
"Content-Type": "application/json",
44+
},
45+
});
46+
47+
const orderData = await response.json();
48+
// Three cases to handle:
49+
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
50+
// (2) Other non-recoverable errors -> Show a failure message
51+
// (3) Successful transaction -> Show confirmation or thank you message
52+
53+
const transaction =
54+
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
55+
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
56+
const errorDetail = orderData?.details?.[0];
57+
58+
// this actions.restart() behavior only applies to the Buttons component
59+
if (errorDetail?.issue === "INSTRUMENT_DECLINED" && !data.card && actions) {
60+
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
61+
// recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
62+
return actions.restart();
63+
} else if (
64+
errorDetail ||
65+
!transaction ||
66+
transaction.status === "DECLINED"
67+
) {
68+
// (2) Other non-recoverable errors -> Show a failure message
69+
let errorMessage;
70+
if (transaction) {
71+
errorMessage = `Transaction ${transaction.status}: ${transaction.id}`;
72+
} else if (errorDetail) {
73+
errorMessage = `${errorDetail.description} (${orderData.debug_id})`;
74+
} else {
75+
errorMessage = JSON.stringify(orderData);
76+
}
77+
78+
throw new Error(errorMessage);
79+
} else {
80+
// (3) Successful transaction -> Show confirmation or thank you message
81+
// Or go to another URL: actions.redirect('thank_you.html');
82+
resultMessage(
83+
`Transaction ${transaction.status}: ${transaction.id}<br><br>See console for all available details`,
84+
);
85+
console.log(
86+
"Capture result",
87+
orderData,
88+
JSON.stringify(orderData, null, 2),
89+
);
90+
}
91+
} catch (error) {
92+
console.error(error);
93+
resultMessage(
94+
`Sorry, your transaction could not be processed...<br><br>${error}`,
95+
);
96+
}
97+
}
98+
99+
window.paypal
100+
.Buttons({
101+
createOrder: createOrderCallback,
102+
onApprove: onApproveCallback,
103+
})
104+
.render("#paypal-button-container");
105+
106+
const cardField = window.paypal.CardFields({
107+
createOrder: createOrderCallback,
108+
onApprove: onApproveCallback,
109+
});
110+
111+
// Render each field after checking for eligibility
112+
if (cardField.isEligible()) {
113+
const nameField = cardField.NameField();
114+
nameField.render("#card-name-field-container");
115+
116+
const numberField = cardField.NumberField();
117+
numberField.render("#card-number-field-container");
118+
119+
const cvvField = cardField.CVVField();
120+
cvvField.render("#card-cvv-field-container");
121+
122+
const expiryField = cardField.ExpiryField();
123+
expiryField.render("#card-expiry-field-container");
124+
125+
// Add click listener to submit button and call the submit function on the CardField component
126+
document
127+
.getElementById("multi-card-field-button")
128+
.addEventListener("click", () => {
129+
cardField.submit().catch((error) => {
130+
resultMessage(
131+
`Sorry, your transaction could not be processed...<br><br>${error}`,
132+
);
133+
});
134+
});
135+
} else {
136+
// Hides card fields if the merchant isn't eligible
137+
document.querySelector("#card-form").style = "display: none";
138+
}
139+
140+
// Example function to show a result to the user. Your site's UI library can be used instead.
141+
function resultMessage(message) {
142+
const container = document.querySelector("#result-message");
143+
container.innerHTML = message;
144+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "paypal-advanced-integration",
3+
"description": "Sample Node.js web app to integrate PayPal Advanced Checkout for online payments",
4+
"version": "1.0.0",
5+
"main": "server/server.js",
6+
"type": "module",
7+
"scripts": {
8+
"test": "echo \"Error: no test specified\" && exit 1",
9+
"start": "nodemon server/server.js",
10+
"format": "npx prettier --write **/*.{js,md}",
11+
"format:check": "npx prettier --check **/*.{js,md}",
12+
"lint": "npx eslint server/*.js --env=node && npx eslint client/*.js --env=browser"
13+
},
14+
"license": "Apache-2.0",
15+
"dependencies": {
16+
"dotenv": "^16.3.1",
17+
"express": "^4.18.2",
18+
"node-fetch": "^3.3.2"
19+
},
20+
"devDependencies": {
21+
"nodemon": "^3.0.1"
22+
}
23+
}

0 commit comments

Comments
 (0)