Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Use ejs to render data-user-id-token
  • Loading branch information
jshawl committed Nov 2, 2023
commit d581019bd5c412076e5ea000e6ef3014dae51a5d
183 changes: 81 additions & 102 deletions save-payment-method/client/app.js
Original file line number Diff line number Diff line change
@@ -1,115 +1,94 @@
const main = async () => {
const user = localStorage.getItem("user");
const response = await fetch(`/api/id-token?user=${user}`);
const data = await response.json();
const script = document.createElement("script");
script.src = "https://www.paypal.com/sdk/js?client-id=test";
script.setAttribute("data-user-id-token", data.id_token);
script.addEventListener("load", () => renderButtons());
document.head.appendChild(script);
};
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",
},
],
}),
});

main();
const orderData = await response.json();

const renderButtons = () =>
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);
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}`,
);
throw new Error(errorMessage);
}
},
async onApprove(data, actions) {
try {
const response = await fetch(`/api/orders/${data.orderID}/capture`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
} catch (error) {
console.error(error);
resultMessage(`Could not initiate PayPal Checkout...<br><br>${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 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}<br><br>See console for all available details`,
);

localStorage.setItem(
"user",
orderData.payment_source.paypal.attributes.vault.customer.id,
);
const errorDetail = orderData?.details?.[0];

console.log(
"Capture result",
orderData,
JSON.stringify(orderData, null, 2),
);
}
} catch (error) {
console.error(error);
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(
`Sorry, your transaction could not be processed...<br><br>${error}`,
`Transaction ${transaction.status}: ${transaction.id}<br><br>See console for all available details.<br>
<a href='/?${orderData.payment_source.paypal.attributes.vault.customer.id}'>See the return buyer experience</a>
`,
);

console.log(
"Capture result",
orderData,
JSON.stringify(orderData, null, 2),
);
}
},
})
.render("#paypal-button-container");
} catch (error) {
console.error(error);
resultMessage(
`Sorry, your transaction could not be processed...<br><br>${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) {
Expand Down
1 change: 1 addition & 0 deletions save-payment-method/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"license": "Apache-2.0",
"dependencies": {
"dotenv": "^16.3.1",
"ejs": "^3.1.9",
"express": "^4.18.2",
"node-fetch": "^3.3.2"
},
Expand Down
24 changes: 15 additions & 9 deletions save-payment-method/server/server.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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();

app.set("view engine", "ejs");
app.set("views", "./server/views");

// host static files
app.use(express.static("client"));

Expand Down Expand Up @@ -143,11 +145,6 @@ async function handleResponse(response) {
}
}

app.get("/api/id-token", async (req, res) => {
const { jsonResponse } = await authenticate(req.query.user);
res.json(jsonResponse);
});

app.post("/api/orders", async (req, res) => {
try {
// use the cart information passed from the front-end to calculate the order amount detals
Expand All @@ -164,16 +161,25 @@ 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." });
}
});

// serve index.html
app.get("/", (req, res) => {
res.sendFile(path.resolve("./client/checkout.html"));
// render checkout page with client id & user id token
app.get("/", async (req, res) => {
try {
const { jsonResponse } = await authenticate(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, () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PayPal JS SDK Standard Integration</title>
<title>PayPal JS SDK Save Payment Method Integration</title>
</head>
<body>
<div id="paypal-button-container"></div>
<p id="result-message"></p>
<script
src="https://www.paypal.com/sdk/js?client-id=<%= clientId %>"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also seeing this warning in the console:
Screenshot 2023-11-03 at 08 51 47

though the docs do not include vault=true in the code sample

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it in d4ea5c3 and the warning went away 🥳

data-user-id-token="<%= userIdToken %>"
></script>
<script src="app.js"></script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you be open to converting this file to .ejs? Wondering if that would make things easier with setting the data-user-id-token.

For the standard integration we have kept it as plain html to keep it as simple as possible. But for other examples we have been leveraging ejs for server-side templating. That way you can embed values that the data-user-id from the server-side and still the load the JS SDK script in it too. Here's an example of how v2 card fields does this to set the clientID: https://github.com/paypal-examples/docs-examples/blob/main/advanced-integration/v2/server/views/checkout.ejs#L20

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah great idea!! yes - I really like this approach!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converted in d581019

</body>
</html>