Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/christenjacob/donationsflow #28

Merged
merged 27 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
274c32b
add react-paypal-js module
jackavh Mar 5, 2024
5ffdcce
moves paypal module to frontend
jackavh Mar 5, 2024
1baace5
temp setup of PaypalContext
jackavh Mar 5, 2024
ee696c8
Paypal buttons n all
christen03 Mar 11, 2024
678e372
style changes
jackavh Mar 12, 2024
d341921
adds monetary/physical selector
jackavh Mar 12, 2024
eca2326
started server side implementations
christen03 Mar 13, 2024
4fa8973
started server side implementations
christen03 Mar 13, 2024
3ea2f1a
Merge branch 'feature/christenjacob/donationsflow' of https://github.…
christen03 Mar 13, 2024
d871344
merge changes from main into donationsflow branch
jackavh Apr 9, 2024
23d7558
physical donations form
jackavh Apr 16, 2024
ba5e60b
create frontend endpoints for paypal
jackavh Apr 17, 2024
80cf866
refactors DonateButton component to use new api endpoints in frontend…
jackavh Apr 17, 2024
6df2d75
fix some documentation errors
jackavh Apr 17, 2024
5ec1989
gets donateButton to work better with the backend api and then set su…
jackavh Apr 17, 2024
66aea80
fix import statements
jackavh Apr 17, 2024
550b8e3
implements custom donation amount picking
jackavh Apr 17, 2024
2f70529
add success graphic
jackavh Apr 17, 2024
cf87cb8
updated amount picker
christen03 Apr 17, 2024
04829b7
fix reload bug
jackavh Apr 19, 2024
372c640
fix donation email
jackavh Apr 19, 2024
7d7535a
lint fixes
jackavh Apr 19, 2024
bc973c4
Minor styling changes to match the figma design
jennymar Apr 24, 2024
f9b1362
Minor frontend border fix
jennymar Apr 24, 2024
85dc7a8
Merge branch 'main' into feature/christenjacob/donationsflow
jennymar May 6, 2024
4cf0521
Frontend linting fix
jennymar May 6, 2024
bc55c12
More frontend linting
jennymar May 7, 2024
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
Binary file added .swp
Binary file not shown.
11 changes: 6 additions & 5 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
"http-errors": "^2.0.0",
"module-alias": "^2.2.3",
"mongodb": "^6.3.0",
"mongoose": "^8.0.3",
"nodemailer": "^6.9.11"
"mongoose": "^8.0.3"
},
"devDependencies": {
"@types/cors": "^2.8.17",
Expand All @@ -36,6 +35,7 @@
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"husky": "^8.0.3",
"nodemailer": "^6.9.13",
"nodemon": "^3.0.2",
"prettier": "^3.1.1",
"ts-node-dev": "^2.0.0",
Expand Down
2 changes: 2 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import volunteerDetailsRoutes from "./routes/volunteerDetails";
import testimonialRoutes from "src/routes/testimonial";
import newsletterRoutes from "src/routes/newsletter"; // Import newsletter routes
import emailRoutes from "src/routes/emails";
import paypalRoutes from "src/routes/paypal";

const app = express();

Expand All @@ -40,6 +41,7 @@ app.use("/api/volunteerDetails", volunteerDetailsRoutes);
app.use("/api/testimonial", testimonialRoutes);
app.use("/api/newsletter", newsletterRoutes);
app.use("/api/emails", emailRoutes);
app.use("/api/orders", paypalRoutes); // Donation Order routes

/**
* Error handler; all errors thrown by server are handled here.
Expand Down
4 changes: 4 additions & 0 deletions backend/src/controllers/emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export const createEmail: RequestHandler = async (req, res, next) => {
const specialStr = specialUpdates ? "Yes" : "No";
EMAIL_SUBJECT = `Newsletter Form: Subscription by ${firstName} ${lastName}`;
EMAIL_BODY = `First Name: ${firstName} \nLast Name: ${lastName}\nEmail: ${email}\nReceive Quarterly Updates: ${quarterlyStr}\nReceive Special Events Updates: ${specialStr}`;
} else if (req.body.type === "donation") {
const { firstName, lastName, email, phone, comment } = req.body;
EMAIL_SUBJECT = `Physical Donation Form: Donation from ${firstName} ${lastName}`;
EMAIL_BODY = `First Name: ${firstName}\nLast Name: ${lastName}\nEmail: ${email}\nPhone Number: ${phone}\nDonation Comment:\n${comment}`;
}

try {
Expand Down
22 changes: 22 additions & 0 deletions backend/src/controllers/paypal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RequestHandler } from "express";
import { captureOrder, createOrder } from "src/services/paypal";

export const createOrderHandler: RequestHandler = async (req, res, next) => {
try {
const { cart } = req.body;
const { jsonResponse, httpStatusCode } = await createOrder(cart);
res.status(httpStatusCode).json(jsonResponse);
} catch (error) {
next(error);
}
};

export const captureOrderHandler: RequestHandler = async (req, res, next) => {
try {
const { orderId } = req.params;
const { jsonResponse, httpStatusCode } = await captureOrder(orderId);
res.status(httpStatusCode).json(jsonResponse);
} catch (error) {
next(error);
}
};
9 changes: 9 additions & 0 deletions backend/src/routes/paypal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import express from "express";
import { captureOrderHandler, createOrderHandler } from "src/controllers/paypal";

const router = express.Router();

router.post("/", createOrderHandler);
router.post("/:orderId/capture", captureOrderHandler);

export default router;
4 changes: 3 additions & 1 deletion backend/src/services/emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ const sendContactEmail = async (subject: string, message: string) => {
const EMAIL_SUBJECT = `${subject}`;
const EMAIL_BODY = `${message}`;
const transporter = nodemailer.createTransport({
service: "gmail",
service: "Gmail",
host: "smtp.gmail.com",
secure: true,
auth: {
user: env.EMAIL_USER,
pass: env.EMAIL_APP_PASSWORD,
Expand Down
125 changes: 125 additions & 0 deletions backend/src/services/paypal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID;
const PAYPAL_CLIENT_SECRET = process.env.PAYPAL_CLIENT_SECRET;

const base = "https://api-m.sandbox.paypal.com";

/**
* Types the CartItem and Cart objects which gets used in a createOrder request
*/
type CartItem = {
id: string;
amount: string; // string dollar amout, eg "5.00"
quantity: string;
};

type Cart = CartItem[];

/**
* Generate an OAuth 2.0 token to authenticate with PayPal REST APIs
* @see https://developer.paypal.com/api/rest/authentication/
*/
const generateAccessToken = async () => {
try {
if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) {
throw new Error("MISSING_PAYPAL_API_CREDENTIALS");
}
const auth = Buffer.from(PAYPAL_CLIENT_ID + ":" + PAYPAL_CLIENT_SECRET).toString("base64");
const response = await fetch(`${base}/v1/oauth2/token`, {
method: "POST",
body: "grant_type=client_credentials",
headers: {
Authorization: `Basic ${auth}`,
},
});

const data = await response.json();
return data.access_token;
} catch (error) {
console.error("Failed to generate PayPal Access Token: ", error);
}
};

async function handleResponse(response: Response) {
try {
const jsonResponse = await response.json();
return {
jsonResponse,
httpStatusCode: response.status,
};
} catch (error) {
const errorMessage = await response.text();
throw new Error(errorMessage);
}
}

export async function createOrder(cart: Cart) {
const accessToken = await generateAccessToken();
const url = `${base}/v2/checkout/orders`;
console.log("shopping cart info", cart);

const payload = {
intent: "CAPTURE",
purchase_units: [
{
items: [
{
name: "Donation to 4FLOT",
description: "Deserunt aliquip enim Lorem.",
quantity: "1",
unit_amount: {
currency_code: "USD",
value: cart[0].amount,
},
category: "DONATION",
},
],
amount: {
currency_code: "USD",
value: cart[0].amount,
breakdown: {
item_total: {
currency_code: "USD",
value: cart[0].amount,
},
},
},
},
],
};

const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
// "PayPal-Mock-Response": '{"mock_application_codes": "MISSING_REQUIRED_PARAMETER"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "PERMISSION_DENIED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
},
method: "POST",
body: JSON.stringify(payload),
});

return handleResponse(response);
}

export async function captureOrder(orderID: string) {
const accessToken = await generateAccessToken();
const url = `${base}/v2/checkout/orders/${orderID}/capture`;

const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
// "PayPal-Mock-Response": '{"mock_application_codes": "INSTRUMENT_DECLINED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "TRANSACTION_REFUSED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
},
});

return handleResponse(response);
}
40 changes: 40 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@mui/x-data-grid": "^7.1.1",
"envalid": "^8.0.0",
"firebase": "^10.11.0",
"@paypal/react-paypal-js": "^8.2.0",
"html2canvas": "^1.4.1",
"html2pdf.js": "^0.9.3",
"jspdf": "^2.5.1",
Expand Down
9 changes: 9 additions & 0 deletions frontend/public/donations/successGraphic.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 14 additions & 1 deletion frontend/src/api/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,21 @@ export type CreateNewsletterEmailRequest = {
specialUpdates: boolean;
};

export type CreateDonationEmailRequest = {
type: string;
firstName: string;
lastName: string;
email: string;
phone: string;
comment: string;
};

export async function sendEmail(
email: CreateContactEmailRequest | CreateVolunteerEmailRequest | CreateNewsletterEmailRequest,
email:
| CreateContactEmailRequest
| CreateVolunteerEmailRequest
| CreateNewsletterEmailRequest
| CreateDonationEmailRequest,
) {
try {
await post("/api/emails", email);
Expand Down
Loading
Loading