diff --git a/features/paypal/api.go b/features/paypal/api.go
index 17e3274..a55fc30 100644
--- a/features/paypal/api.go
+++ b/features/paypal/api.go
@@ -7,41 +7,72 @@ import (
"context"
. "MY/webapp/common"
config "MY/webapp/config"
+ "fmt"
)
-func GetApiStart(c *fiber.Ctx) error {
- ctx := context.Background()
- pay, err := paypal.NewClient(
+func CreatePaypal() (*paypal.Client, error) {
+ return paypal.NewClient(
config.Settings.Paypal.ClientID,
config.Settings.Paypal.SecretID,
config.Settings.Paypal.URL) // or paypal.APIBaseLive
+}
+
+func PostApiOrder(c *fiber.Ctx) error {
+ pay, err := CreatePaypal()
+
if err != nil { return IfErrNil(err, c) }
pay.SetLog(os.Stdout)
- pay.Token = &paypal.TokenResponse{Token: "dummy"}
-
- payout := paypal.Payout{
- Items: []paypal.PayoutItem{
- {
- Amount: &paypal.AmountPayout{
- Currency: "USD",
- Value: "10.00",
+ units := []paypal.PurchaseUnitRequest{
+ {
+ ReferenceID: "myinternalid1",
+ Amount: &paypal.PurchaseUnitAmount{
+ Currency: "USD",
+ Value: "10.99",
+ },
+ Description: "Product description",
+ Items: []paypal.Item{
+ {
+ Name: "Learn Go the Hard Way",
+ UnitAmount: &paypal.Money{
+ Currency: "USD",
+ Value: "10.99",
+ },
+ Quantity: "1",
},
- Receiver: "test@example.com",
},
},
- SenderBatchHeader: &paypal.SenderBatchHeader{
- SenderBatchID: "BATCH123",
- },
}
- response, err := pay.CreatePayout(ctx, payout)
+ source := &paypal.PaymentSource{}
+ appCtx := &paypal.ApplicationContext{}
+
+ order, err := pay.CreateOrder(context.TODO(), paypal.OrderIntentCapture, units, source, appCtx)
+
+ fmt.Println("ORDER", order)
+
+ return c.JSON(order)
+}
+
+func PostApiOrderCapture(c *fiber.Ctx) error {
+ orderID := c.Params("orderID")
+
+ fmt.Println("POST ORDER CAPTURE", orderID)
+ pay, err := CreatePaypal()
+ if err != nil { return IfErrNil(err, c) }
+
+ capture, err := pay.CaptureOrder(context.TODO(), orderID, paypal.CaptureOrderRequest{})
if err != nil { return IfErrNil(err, c) }
- return c.JSON(response)
+ fmt.Println("CAPTURE", capture)
+
+ return c.JSON(fiber.Map{"status": "ok"})
}
func SetupApi(app *fiber.App) {
- app.Get("/api/paypal/start", GetApiStart)
+ app.Post("/api/paypal/order", PostApiOrder)
+ app.Post("/api/paypal/order/:orderID/capture", PostApiOrderCapture)
}
+
+
diff --git a/features/paypal/api.js b/features/paypal/api.js
new file mode 100644
index 0000000..f8c704e
--- /dev/null
+++ b/features/paypal/api.js
@@ -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}/`);
+});
+
diff --git a/features/paypal/init.go b/features/paypal/init.go
index b7ae1a5..0958d4a 100644
--- a/features/paypal/init.go
+++ b/features/paypal/init.go
@@ -2,43 +2,9 @@ package features_paypal
import (
"github.com/gofiber/fiber/v2"
-
- "net/http"
- "net/http/httptest"
-
- mockserver "github.com/plutov/paypal/v4/mockserver"
- srvPayouts "github.com/plutov/paypal/v4/mockserver/payments_payouts_batch_v1"
- srvShipping "github.com/plutov/paypal/v4/mockserver/shipping_shipment_tracking_v1"
-
- "MY/webapp/config"
)
-var PAYPAL *httptest.Server = nil
-
-func createTestServer() *httptest.Server {
- mock := &mockserver.MockServer{}
- mux := http.NewServeMux()
-
- payoutsSI := srvPayouts.NewStrictHandler(mock, nil)
- srvPayouts.HandlerWithOptions(payoutsSI, srvPayouts.StdHTTPServerOptions{
- BaseRouter: mux,
- })
-
- shippingSI := srvShipping.NewStrictHandler(mock, nil)
- srvShipping.HandlerWithOptions(shippingSI, srvShipping.StdHTTPServerOptions{
- BaseRouter: mux,
- })
-
- return httptest.NewServer(mux)
-}
-
func Setup(app *fiber.App) {
- // if you don't set the URL then use a fake mock
- if config.Settings.Paypal.URL == "" {
- PAYPAL = createTestServer()
- config.Settings.Paypal.URL = PAYPAL.URL
- }
-
SetupApi(app)
SetupViews(app)
}
diff --git a/static/js/paypal_app.js b/static/js/paypal_app.js
new file mode 100644
index 0000000..bfc40aa
--- /dev/null
+++ b/static/js/paypal_app.js
@@ -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...
${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}
+
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...
${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;
+}
+
diff --git a/static/style.css b/static/style.css
index 68642d0..c4a5147 100644
--- a/static/style.css
+++ b/static/style.css
@@ -243,6 +243,24 @@
.left-40 {
left: calc(var(--spacing) * 40);
}
+ .container {
+ width: 100%;
+ @media (width >= 40rem) {
+ max-width: 40rem;
+ }
+ @media (width >= 48rem) {
+ max-width: 48rem;
+ }
+ @media (width >= 64rem) {
+ max-width: 64rem;
+ }
+ @media (width >= 80rem) {
+ max-width: 80rem;
+ }
+ @media (width >= 96rem) {
+ max-width: 96rem;
+ }
+ }
.block {
display: block;
}
@@ -267,9 +285,6 @@
.aspect-\[9\/12\]\! {
aspect-ratio: 9/12 !important;
}
- .aspect-square {
- aspect-ratio: 1 / 1;
- }
.aspect-square\! {
aspect-ratio: 1 / 1 !important;
}
@@ -309,15 +324,9 @@
.min-w-md {
min-width: var(--container-md);
}
- .border-collapse {
- border-collapse: collapse;
- }
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
- .resize {
- resize: both;
- }
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -345,9 +354,6 @@
.justify-evenly {
justify-content: space-evenly;
}
- .gap-0 {
- gap: calc(var(--spacing) * 0);
- }
.gap-0\! {
gap: calc(var(--spacing) * 0) !important;
}
@@ -406,10 +412,6 @@
}
}
}
- .border {
- border-style: var(--tw-border-style);
- border-width: 1px;
- }
.border-1 {
border-style: var(--tw-border-style);
border-width: 1px;
@@ -474,9 +476,6 @@
.bg-gray-700 {
background-color: var(--color-gray-700);
}
- .bg-green-400 {
- background-color: var(--color-green-400);
- }
.bg-green-400\! {
background-color: var(--color-green-400) !important;
}
@@ -486,18 +485,12 @@
.\!p-4 {
padding: calc(var(--spacing) * 4) !important;
}
- .p-0 {
- padding: calc(var(--spacing) * 0);
- }
.p-0\! {
padding: calc(var(--spacing) * 0) !important;
}
.p-1 {
padding: calc(var(--spacing) * 1);
}
- .p-2 {
- padding: calc(var(--spacing) * 2);
- }
.p-2\! {
padding: calc(var(--spacing) * 2) !important;
}
@@ -510,9 +503,6 @@
.pt-4 {
padding-top: calc(var(--spacing) * 4);
}
- .pb-0 {
- padding-bottom: calc(var(--spacing) * 0);
- }
.pb-0\! {
padding-bottom: calc(var(--spacing) * 0) !important;
}
@@ -531,9 +521,6 @@
.pb-8 {
padding-bottom: calc(var(--spacing) * 8);
}
- .pb-10 {
- padding-bottom: calc(var(--spacing) * 10);
- }
.pb-10\! {
padding-bottom: calc(var(--spacing) * 10) !important;
}
@@ -570,9 +557,6 @@
--tw-font-weight: var(--font-weight-light);
font-weight: var(--font-weight-light);
}
- .text-wrap {
- text-wrap: wrap;
- }
.text-gray-50 {
color: var(--color-gray-50);
}
@@ -588,17 +572,10 @@
.text-red-500 {
color: var(--color-red-500);
}
- .underline {
- text-decoration-line: underline;
- }
.shadow-lg {
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
- .outline {
- outline-style: var(--tw-outline-style);
- outline-width: 1px;
- }
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
diff --git a/views/paypal/index.html b/views/paypal/index.html
index 9c8edfe..91fef5f 100644
--- a/views/paypal/index.html
+++ b/views/paypal/index.html
@@ -1,3 +1,21 @@
-
Form goes here:
+ + + + + + + + + + -Replace me.