parent
9709c2434e
commit
e011a8c333
@ -0,0 +1,160 @@ |
|||||||
|
import express from "express"; |
||||||
|
import "dotenv/config"; |
||||||
|
import { |
||||||
|
ApiError, |
||||||
|
CheckoutPaymentIntent, |
||||||
|
Client, |
||||||
|
Environment, |
||||||
|
LogLevel, |
||||||
|
OrdersController, |
||||||
|
PaymentsController, |
||||||
|
PaypalExperienceLandingPage, |
||||||
|
PaypalExperienceUserAction, |
||||||
|
ShippingPreference, |
||||||
|
} from "@paypal/paypal-server-sdk"; |
||||||
|
import bodyParser from "body-parser"; |
||||||
|
|
||||||
|
const app = express(); |
||||||
|
app.use(bodyParser.json()); |
||||||
|
|
||||||
|
const { |
||||||
|
PAYPAL_CLIENT_ID, |
||||||
|
PAYPAL_CLIENT_SECRET, |
||||||
|
PORT = 8080, |
||||||
|
} = process.env; |
||||||
|
|
||||||
|
const client = new Client({ |
||||||
|
clientCredentialsAuthCredentials: { |
||||||
|
oAuthClientId: PAYPAL_CLIENT_ID, |
||||||
|
oAuthClientSecret: PAYPAL_CLIENT_SECRET, |
||||||
|
}, |
||||||
|
timeout: 0, |
||||||
|
environment: Environment.Sandbox, |
||||||
|
logging: { |
||||||
|
logLevel: LogLevel.Info, |
||||||
|
logRequest: { logBody: true }, |
||||||
|
logResponse: { logHeaders: true }, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
const ordersController = new OrdersController(client); |
||||||
|
const paymentsController = new PaymentsController(client); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Create an order to start the transaction. |
||||||
|
* @see https://developer.paypal.com/docs/api/orders/v2/#orders_create
|
||||||
|
*/ |
||||||
|
const createOrder = async (cart) => { |
||||||
|
const collect = { |
||||||
|
body: { |
||||||
|
intent: "CAPTURE", |
||||||
|
purchaseUnits: [ |
||||||
|
{ |
||||||
|
amount: { |
||||||
|
currencyCode: "USD", |
||||||
|
value: "100", |
||||||
|
breakdown: { |
||||||
|
itemTotal: { |
||||||
|
currencyCode: "USD", |
||||||
|
value: "100", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
// lookup item details in `cart` from database
|
||||||
|
items: [ |
||||||
|
{ |
||||||
|
name: "T-Shirt", |
||||||
|
unitAmount: { |
||||||
|
currencyCode: "USD", |
||||||
|
value: "100", |
||||||
|
}, |
||||||
|
quantity: "1", |
||||||
|
description: "Super Fresh Shirt", |
||||||
|
sku: "sku01", |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
prefer: "return=minimal", |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
try { |
||||||
|
const { body, ...httpResponse } = await ordersController.createOrder( |
||||||
|
collect |
||||||
|
); |
||||||
|
// Get more response info...
|
||||||
|
// const { statusCode, headers } = httpResponse;
|
||||||
|
return { |
||||||
|
jsonResponse: JSON.parse(body), |
||||||
|
httpStatusCode: httpResponse.statusCode, |
||||||
|
}; |
||||||
|
} catch (error) { |
||||||
|
if (error instanceof ApiError) { |
||||||
|
// const { statusCode, headers } = error;
|
||||||
|
throw new Error(error.message); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// createOrder route
|
||||||
|
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." }); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* 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 collect = { |
||||||
|
id: orderID, |
||||||
|
prefer: "return=minimal", |
||||||
|
}; |
||||||
|
|
||||||
|
try { |
||||||
|
const { body, ...httpResponse } = await ordersController.captureOrder( |
||||||
|
collect |
||||||
|
); |
||||||
|
// Get more response info...
|
||||||
|
// const { statusCode, headers } = httpResponse;
|
||||||
|
return { |
||||||
|
jsonResponse: JSON.parse(body), |
||||||
|
httpStatusCode: httpResponse.statusCode, |
||||||
|
}; |
||||||
|
} catch (error) { |
||||||
|
if (error instanceof ApiError) { |
||||||
|
// const { statusCode, headers } = error;
|
||||||
|
throw new Error(error.message); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// captureOrder route
|
||||||
|
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}/`); |
||||||
|
}); |
||||||
|
|
||||||
@ -0,0 +1,113 @@ |
|||||||
|
const paypalButtons = window.paypal.Buttons({ |
||||||
|
style: { |
||||||
|
shape: "rect", |
||||||
|
layout: "vertical", |
||||||
|
color: "gold", |
||||||
|
label: "pay", |
||||||
|
}, |
||||||
|
message: { |
||||||
|
amount: 100, |
||||||
|
}, |
||||||
|
async createOrder() { |
||||||
|
try { |
||||||
|
const response = await fetch("/api/paypal/order", { |
||||||
|
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; |
||||||
|
} |
||||||
|
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 onApprove(data, actions) { |
||||||
|
try { |
||||||
|
const response = await fetch( |
||||||
|
`/api/paypal/order/${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}<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}` |
||||||
|
); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
|
||||||
|
}); |
||||||
|
paypalButtons.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; |
||||||
|
} |
||||||
|
|
||||||
Loading…
Reference in new issue