Initial setup for paypal but can't go further until I get the sandbox setup.

master
Zed A. Shaw 3 weeks ago
parent 9709c2434e
commit e011a8c333
  1. 61
      features/paypal/api.go
  2. 160
      features/paypal/api.js
  3. 34
      features/paypal/init.go
  4. 113
      static/js/paypal_app.js
  5. 59
      static/style.css
  6. 22
      views/paypal/index.html

@ -7,41 +7,72 @@ import (
"context" "context"
. "MY/webapp/common" . "MY/webapp/common"
config "MY/webapp/config" config "MY/webapp/config"
"fmt"
) )
func GetApiStart(c *fiber.Ctx) error { func CreatePaypal() (*paypal.Client, error) {
ctx := context.Background() return paypal.NewClient(
pay, err := paypal.NewClient(
config.Settings.Paypal.ClientID, config.Settings.Paypal.ClientID,
config.Settings.Paypal.SecretID, config.Settings.Paypal.SecretID,
config.Settings.Paypal.URL) // or paypal.APIBaseLive config.Settings.Paypal.URL) // or paypal.APIBaseLive
}
func PostApiOrder(c *fiber.Ctx) error {
pay, err := CreatePaypal()
if err != nil { return IfErrNil(err, c) } if err != nil { return IfErrNil(err, c) }
pay.SetLog(os.Stdout) pay.SetLog(os.Stdout)
pay.Token = &paypal.TokenResponse{Token: "dummy"} units := []paypal.PurchaseUnitRequest{
payout := paypal.Payout{
Items: []paypal.PayoutItem{
{ {
Amount: &paypal.AmountPayout{ ReferenceID: "myinternalid1",
Amount: &paypal.PurchaseUnitAmount{
Currency: "USD", Currency: "USD",
Value: "10.00", Value: "10.99",
}, },
Receiver: "test@example.com", Description: "Product description",
Items: []paypal.Item{
{
Name: "Learn Go the Hard Way",
UnitAmount: &paypal.Money{
Currency: "USD",
Value: "10.99",
},
Quantity: "1",
}, },
}, },
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) } if err != nil { return IfErrNil(err, c) }
return c.JSON(response) capture, err := pay.CaptureOrder(context.TODO(), orderID, paypal.CaptureOrderRequest{})
if err != nil { return IfErrNil(err, c) }
fmt.Println("CAPTURE", capture)
return c.JSON(fiber.Map{"status": "ok"})
} }
func SetupApi(app *fiber.App) { 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)
} }

@ -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}/`);
});

@ -2,43 +2,9 @@ package features_paypal
import ( import (
"github.com/gofiber/fiber/v2" "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) { 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) SetupApi(app)
SetupViews(app) SetupViews(app)
} }

@ -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;
}

@ -243,6 +243,24 @@
.left-40 { .left-40 {
left: calc(var(--spacing) * 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 { .block {
display: block; display: block;
} }
@ -267,9 +285,6 @@
.aspect-\[9\/12\]\! { .aspect-\[9\/12\]\! {
aspect-ratio: 9/12 !important; aspect-ratio: 9/12 !important;
} }
.aspect-square {
aspect-ratio: 1 / 1;
}
.aspect-square\! { .aspect-square\! {
aspect-ratio: 1 / 1 !important; aspect-ratio: 1 / 1 !important;
} }
@ -309,15 +324,9 @@
.min-w-md { .min-w-md {
min-width: var(--container-md); min-width: var(--container-md);
} }
.border-collapse {
border-collapse: collapse;
}
.transform { .transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); 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-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
@ -345,9 +354,6 @@
.justify-evenly { .justify-evenly {
justify-content: space-evenly; justify-content: space-evenly;
} }
.gap-0 {
gap: calc(var(--spacing) * 0);
}
.gap-0\! { .gap-0\! {
gap: calc(var(--spacing) * 0) !important; gap: calc(var(--spacing) * 0) !important;
} }
@ -406,10 +412,6 @@
} }
} }
} }
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.border-1 { .border-1 {
border-style: var(--tw-border-style); border-style: var(--tw-border-style);
border-width: 1px; border-width: 1px;
@ -474,9 +476,6 @@
.bg-gray-700 { .bg-gray-700 {
background-color: var(--color-gray-700); background-color: var(--color-gray-700);
} }
.bg-green-400 {
background-color: var(--color-green-400);
}
.bg-green-400\! { .bg-green-400\! {
background-color: var(--color-green-400) !important; background-color: var(--color-green-400) !important;
} }
@ -486,18 +485,12 @@
.\!p-4 { .\!p-4 {
padding: calc(var(--spacing) * 4) !important; padding: calc(var(--spacing) * 4) !important;
} }
.p-0 {
padding: calc(var(--spacing) * 0);
}
.p-0\! { .p-0\! {
padding: calc(var(--spacing) * 0) !important; padding: calc(var(--spacing) * 0) !important;
} }
.p-1 { .p-1 {
padding: calc(var(--spacing) * 1); padding: calc(var(--spacing) * 1);
} }
.p-2 {
padding: calc(var(--spacing) * 2);
}
.p-2\! { .p-2\! {
padding: calc(var(--spacing) * 2) !important; padding: calc(var(--spacing) * 2) !important;
} }
@ -510,9 +503,6 @@
.pt-4 { .pt-4 {
padding-top: calc(var(--spacing) * 4); padding-top: calc(var(--spacing) * 4);
} }
.pb-0 {
padding-bottom: calc(var(--spacing) * 0);
}
.pb-0\! { .pb-0\! {
padding-bottom: calc(var(--spacing) * 0) !important; padding-bottom: calc(var(--spacing) * 0) !important;
} }
@ -531,9 +521,6 @@
.pb-8 { .pb-8 {
padding-bottom: calc(var(--spacing) * 8); padding-bottom: calc(var(--spacing) * 8);
} }
.pb-10 {
padding-bottom: calc(var(--spacing) * 10);
}
.pb-10\! { .pb-10\! {
padding-bottom: calc(var(--spacing) * 10) !important; padding-bottom: calc(var(--spacing) * 10) !important;
} }
@ -570,9 +557,6 @@
--tw-font-weight: var(--font-weight-light); --tw-font-weight: var(--font-weight-light);
font-weight: var(--font-weight-light); font-weight: var(--font-weight-light);
} }
.text-wrap {
text-wrap: wrap;
}
.text-gray-50 { .text-gray-50 {
color: var(--color-gray-50); color: var(--color-gray-50);
} }
@ -588,17 +572,10 @@
.text-red-500 { .text-red-500 {
color: var(--color-red-500); color: var(--color-red-500);
} }
.underline {
text-decoration-line: underline;
}
.shadow-lg { .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)); --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); 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 {
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-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)); transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));

@ -1,3 +1,21 @@
<h1>paypal</h1> <h1>Pay with Paypal</h1>
<p>Form goes here:</p>
<div id="paypal-button-container"></div>
<p id="result-message"></p>
<!-- Initialize the JS-SDK -->
<script
src="https://www.paypal.com/sdk/js?client-id=test&buyer-country=US&currency=USD&components=buttons&enable-funding=card&disable-funding=venmo,paylater"
data-sdk-integration-source="developer-studio"
></script>
<script src="/js/paypal_app.js"></script>
<p>Replace me.</p>

Loading…
Cancel
Save