diff --git a/backend/api/urls.py b/backend/api/urls.py index 3ca50c7..9e88ffb 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -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') ] diff --git a/backend/api/views/__init__.py b/backend/api/views/__init__.py index 573d254..3b68186 100644 --- a/backend/api/views/__init__.py +++ b/backend/api/views/__init__.py @@ -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 \ No newline at end of file diff --git a/backend/api/views/donation_handler.py b/backend/api/views/donation_handler.py index 9926daa..e9a4ea5 100644 --- a/backend/api/views/donation_handler.py +++ b/backend/api/views/donation_handler.py @@ -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"] diff --git a/backend/api/views/success_handler.py b/backend/api/views/success_handler.py new file mode 100644 index 0000000..717962b --- /dev/null +++ b/backend/api/views/success_handler.py @@ -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) diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 061ad2b..0000000 --- a/package-lock.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "StockCharity", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "js-cookie": "^3.0.5" - } - }, - "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "license": "MIT", - "engines": { - "node": ">=14" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 26ac64e..0000000 --- a/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "js-cookie": "^3.0.5" - } -} diff --git a/webapp/src/pages/donate.tsx b/webapp/src/pages/donate.tsx index 3373ac3..09d82e3 100644 --- a/webapp/src/pages/donate.tsx +++ b/webapp/src/pages/donate.tsx @@ -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; @@ -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(null); - const [cookies] = useCookies(['token']); const [charities, setCharities] = useState([]); - + + 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', { @@ -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 { @@ -61,6 +85,8 @@ const DonatePage: FC = () => { } }; + // Block rendering until auth is confirmed + if (!authChecked) return null; return (
{/* LEFT: Donation Section */} diff --git a/webapp/src/pages/success.tsx b/webapp/src/pages/success.tsx index 99c74aa..8968d99 100644 --- a/webapp/src/pages/success.tsx +++ b/webapp/src/pages/success.tsx @@ -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; @@ -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(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(cookies.token); - const userId = decoded.user_id; + const decoded = jwtDecode(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/ 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 (

Thank You for Your Donation!

Your donation was processed successfully.

- {session_id && ( + {sessionId && (

Checkout Session ID:

-

{session_id}

+

{sessionId}

)}