Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion backend/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
name="payment-recieved-email",
),
path("check-dividends", check_dividends, name="check-dividends"),
path('donation', donation_handler, name='donation')
path('donation', donation_handler, name='donation'),
path('success', success_handler, name='success')
]
1 change: 1 addition & 0 deletions backend/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from .send_payment_recieved_email import send_payment_recieved_email
from .check_dividends import check_dividends
from .donation_handler import donation_handler
from .success_handler import success_handler
8 changes: 4 additions & 4 deletions backend/api/views/donation_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ def amount_to_price_id(fixed_donation, donate_amount):
# They should probably be in an .env file
match (fixed_donation, donate_amount):
case ("true", "10"):
return "price_1QwQhALtztZ9KxoQMge74pGP"
return os.environ["10_DOLLAR_DONATION_PRICE_ID"]
case ("true", "25"):
return "price_1QwQiOLtztZ9KxoQXzfxdftf"
return os.environ["25_DOLLAR_DONATION_PRICE_ID"]
case ("true", "50"):
return "price_1QwQioLtztZ9KxoQdil8ZPj0"
return os.environ["50_DOLLAR_DONATION_PRICE_ID"]
case ("false", _):
return "price_1QuMCdLtztZ9KxoQXBt26UDy"
return os.environ["ANY_DOLLAR_DONATION_PRICE_ID"]
52 changes: 52 additions & 0 deletions backend/api/views/success_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from django.utils.timezone import now
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
import stripe
from django.conf import settings
from orders.models import DonationOrder
import os

stripe.api_key = os.environ["STRIPE_SECRET_KEY"]


@api_view(['POST'])
def success_handler(request):
"""
Handles successful Stripe donations.
Expects body: { session_id: string }
"""
print("Success handler called")

# Check if the user is authenticated
user = request.user
if not user.is_authenticated:
return Response({'error': 'Requires auth'}, status=status.HTTP_401_UNAUTHORIZED)

# Check if the request contains a session_id
session_id = request.data.get('session_id')
if not session_id:
return Response({'error': 'Requires stripe session id'}, status=status.HTTP_400_BAD_REQUEST)

print(f"Parsed session_id: {session_id}")

return Response({'message': 'Success handler called'}, status=status.HTTP_200_OK)

# https://docs.stripe.com/api/checkout/sessions
try:
checkout_session = stripe.checkout.Session.retrieve(session_id)
amount_total = checkout_session.amount_total / 100 # Convert cents to dollars
DonationOrder.objects.create(
amount=amount_total,
account=user,
date=now(),
status='completed',
stripe_transaction_id=checkout_session.payment_intent # https://docs.stripe.com/api/payment_intents
)
user.total_donations += amount_total
user.save()
return Response({'message': 'Donation recorded successfully.'}, status=status.HTTP_201_CREATED)
except stripe.error.StripeError as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response({'error': 'An unexpected error occurred.'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
21 changes: 0 additions & 21 deletions package-lock.json

This file was deleted.

5 changes: 0 additions & 5 deletions package.json

This file was deleted.

44 changes: 35 additions & 9 deletions webapp/src/pages/donate.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
"use client";
import { FC, useState, useEffect } from 'react';
import axios from 'axios';
import { useCookies } from 'react-cookie';
import { fetchDonations, fetchDonationAmount } from '@/util/charity';
import { isLoggedIn } from "@/util/request"; // for auth checking
import { useRouter } from "next/navigation";
import Cookie from 'js-cookie';

interface DonationData {
amount: number;
}

interface StripeSessionResponse {
url: string;
error?: string;
}

interface CharityData {
id: number;
logo_url: string;
Expand All @@ -16,24 +24,39 @@ interface CharityData {
}

const DonatePage: FC = () => {
const router = useRouter();
const [authChecked, setAuthChecked] = useState(false);
const [loading, setLoading] = useState(false);
const [donationData, setDonationData] = useState<DonationData | null>(null);
const [cookies] = useCookies(['token']);
const [charities, setCharities] = useState<CharityData[]>([]);



useEffect(() => {
isLoggedIn()
.then(() => setAuthChecked(true))
.catch(() => router.push("/login"));
}, []);

useEffect(() => {
if (!authChecked) return;
const loadData = async () => {
const donationResult = await fetchDonations();
setDonationData(donationResult);

const charityList = await fetchDonationAmount();
setCharities(charityList);
};

loadData();
}, []);

}, [authChecked]);
const handleDonate = async (fixed: string, amount: string) => {
const token = Cookie.get('token');
if (!token) {
router.push("/login"); // or show a toast or alert
return;
}

setLoading(true);
try {
const response = await axios.post('http://localhost:8000/api/donation', {
Expand All @@ -42,13 +65,14 @@ const DonatePage: FC = () => {
}, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${cookies.token}`
'Authorization': `Bearer ${token}`
},
responseType: 'text',
validateStatus: () => true
});

const data = JSON.parse(response.data);
const data: StripeSessionResponse = JSON.parse(response.data);

if (data.url) {
window.location.href = data.url;
} else {
Expand All @@ -61,6 +85,8 @@ const DonatePage: FC = () => {
}
};

// Block rendering until auth is confirmed
if (!authChecked) return null;
return (
<div className="min-h-screen flex flex-col lg:flex-row">
{/* LEFT: Donation Section */}
Expand Down
48 changes: 32 additions & 16 deletions webapp/src/pages/success.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// pages/success.tsx
import { useRouter } from 'next/router';
import { FC, useEffect } from 'react';
import { FC, useEffect, useState } from 'react';
import axios from 'axios';
import { jwtDecode } from 'jwt-decode';
import { useCookies } from 'react-cookie';
import Cookie from 'js-cookie';

interface JwtPayload {
user_id: number;
Expand All @@ -12,49 +12,65 @@ interface JwtPayload {
// /success?session_id=
const SuccessPage: FC = () => {
const router = useRouter();
const { session_id } = router.query;
const [cookies] = useCookies(['token']);
const [sessionId, setSessionId] = useState<string | null>(null);
const token = Cookie.get('token');

// this is to get the query param session_id from the URL
// why is it so roundabout? because retrieving query params is kinda weird
// doing const { session_id } = router.query; results in session_id undefined sometimes
useEffect(() => {
async function sendEmail() {
if (!cookies.token) return;
if (!router.isReady) return;

const { session_id } = router.query;
if (typeof session_id === 'string') {
setSessionId(session_id);
}
}, [router.isReady, router.query]); // this seems to set sessionId reliably

useEffect(() => {
async function handle_sucess_donation() {
if (!token || !sessionId) return;

try {
// decoding the JWT and extract the user id
const decoded = jwtDecode<JwtPayload>(cookies.token);
const userId = decoded.user_id;
const decoded = jwtDecode<JwtPayload>(token);
const user_id = decoded.user_id;

const config = {
headers: {
Authorization: `Bearer ${cookies.token}`,
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
};

// request the account info from /account/<userId> endpoint
const accountResponse = await axios.get(`http://localhost:8000/account/${userId}/`, config);
const { email } = accountResponse.data;
const accountResponse = await axios.get(`http://localhost:8000/account/${user_id}/`, config);
const { email } = await accountResponse.data;
console.log(email);

// why does the api endpoint also require charity name?
const charity = "Lorem Ipsum";

// making request to backend
await axios.post('http://localhost:8000/api/send-payment-recieved-email', { receiver: email, charity });

console.log("Stripe session ID:", sessionId);
await axios.post('http://localhost:8000/api/success', { session_id: sessionId }, config);
} catch (error) {
console.error("Error sending payment received email:", error);
// TODO: Still need to add the donation record to the DB somehow
}
}
sendEmail();
}, []);
handle_sucess_donation();
}, [sessionId, token]);

return (
<div className="flex flex-col items-center justify-center min-h-screen py-8 bg-gray-50">
<h1 className="text-3xl font-bold mb-4">Thank You for Your Donation!</h1>
<p className="text-lg mb-6">Your donation was processed successfully.</p>
{session_id && (
{sessionId && (
<div className="bg-white shadow p-4 rounded border border-gray-200">
<p className="font-semibold mb-1">Checkout Session ID:</p>
<p className="text-sm break-words">{session_id}</p>
<p className="text-sm break-words">{sessionId}</p>
</div>
)}
<button
Expand Down