From 4ddcb3e1390ddb59034e5fe2b8eb3d8babb32899 Mon Sep 17 00:00:00 2001 From: JustIceO7 Date: Sun, 16 Feb 2025 03:53:04 +0000 Subject: [PATCH] FEAT: Added Stripe webhook to handle user subscriptions UPDATE: Improved the chat look --- frontend/public/images/pfp/monkey.png | Bin 0 -> 5281 bytes .../src/components/Checkout/CheckoutForm.tsx | 6 ++- frontend/src/components/Video/ChatPanel.tsx | 48 +++++++++++------ frontend/src/pages/VideoPage.tsx | 2 +- web_server/blueprints/stripe.py | 50 +++++++++++++++--- web_server/blueprints/user.py | 11 ---- web_server/utils/user_utils.py | 19 +++---- 7 files changed, 89 insertions(+), 47 deletions(-) create mode 100644 frontend/public/images/pfp/monkey.png diff --git a/frontend/public/images/pfp/monkey.png b/frontend/public/images/pfp/monkey.png new file mode 100644 index 0000000000000000000000000000000000000000..2be60e8c050bc8095ae80cdf5d3b8ef079831ddb GIT binary patch literal 5281 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 literal 0 HcmV?d00001 diff --git a/frontend/src/components/Checkout/CheckoutForm.tsx b/frontend/src/components/Checkout/CheckoutForm.tsx index c304bbd..1b79a52 100644 --- a/frontend/src/components/Checkout/CheckoutForm.tsx +++ b/frontend/src/components/Checkout/CheckoutForm.tsx @@ -21,9 +21,11 @@ export const Return: React.FC = () => { const sessionId = urlParams.get("session_id"); if (sessionId) { + console.log("1") fetch(`/api/session-status?session_id=${sessionId}`) .then((res) => res.json()) .then((data) => { + console.log("Response Data:", data); setStatus(data.status); setCustomerEmail(data.customer_email); }); @@ -54,9 +56,9 @@ interface CheckoutFormProps { onClose: () => void; } -const CheckoutForm: React.FC = ({ onClose }) => { +const CheckoutForm: React.FC = ({ onClose, streamerID }) => { const fetchClientSecret = () => { - return fetch(`/api/create-checkout-session`, { + return fetch(`/api/create-checkout-session?streamer_id=${streamerID}`, { method: "POST", }) .then((res) => res.json()) diff --git a/frontend/src/components/Video/ChatPanel.tsx b/frontend/src/components/Video/ChatPanel.tsx index 23a77ab..8874a40 100644 --- a/frontend/src/components/Video/ChatPanel.tsx +++ b/frontend/src/components/Video/ChatPanel.tsx @@ -117,32 +117,48 @@ const ChatPanel: React.FC = ({ className="max-w-[30vw] h-full flex flex-col rounded-lg p-4" style={{ gridArea: "1 / 2 / 3 / 3" }} > -

Stream Chat

+

Stream Chat

{messages.map((msg, index) => (
- - {" "} - {msg.chatter_username}:{" "} - - {msg.message} - + {/* User avatar with image */} +
+ User Avatar +
+ +
+
+ {/* Username */} + + {msg.chatter_username} + +
+ {/* Message content */} +
{msg.message}
+
+ + {/* Time sent */} +
{new Date(msg.time_sent).toLocaleTimeString()} - +
))}
diff --git a/frontend/src/pages/VideoPage.tsx b/frontend/src/pages/VideoPage.tsx index 8388d04..f82ded1 100644 --- a/frontend/src/pages/VideoPage.tsx +++ b/frontend/src/pages/VideoPage.tsx @@ -213,7 +213,7 @@ const VideoPage: React.FC = ({ streamerId }) => { - {showCheckout && setShowCheckout(false)} />} + {showCheckout && setShowCheckout(false)} streamerID={streamerId}/>} {showReturn && } {showAuthModal && setShowAuthModal(false)} />} diff --git a/web_server/blueprints/stripe.py b/web_server/blueprints/stripe.py index 4ea3ef7..2dd1ec8 100644 --- a/web_server/blueprints/stripe.py +++ b/web_server/blueprints/stripe.py @@ -1,32 +1,41 @@ -from flask import Blueprint, request, jsonify +from flask import Blueprint, request, jsonify, session as s +from blueprints.middleware import login_required +from utils.user_utils import subscribe import os, stripe stripe_bp = Blueprint("stripe", __name__) stripe.api_key = os.getenv("STRIPE_SECRET_KEY") +endpoint_secret = "" +subscription = os.getenv("GANDER_SUBSCRIPTION") + +@login_required @stripe_bp.route('/create-checkout-session', methods=['POST']) def create_checkout_session(): """ Creates the stripe checkout session """ - print("Creating checkout session") + print("Creating checkout session", flush=True) try: + user_id = s.get("user_id") + streamer_id = request.args.get("streamer_id") session = stripe.checkout.Session.create( ui_mode = 'embedded', payment_method_types=['card'], line_items=[ { - 'price': 'price_1QikNCGk6yuk3uA86mZf3dmM', #Subscription ID + 'price': 'price_1QikNCGk6yuk3uA86mZf3dmM', 'quantity': 1, }, ], mode='subscription', - redirect_on_completion = 'never' + redirect_on_completion = 'never', + client_reference_id = f"{user_id}-{streamer_id}" ) except Exception as e: - print(e) - return str(e) + print(e, flush=True) + return str(e), 500 return jsonify(clientSecret=session.client_secret) @@ -38,3 +47,32 @@ def session_status(): session = stripe.checkout.Session.retrieve(request.args.get('session_id')) return jsonify(status=session.status, customer_email=session.customer_details.email) + +@stripe_bp.route('/stripe/webhook', methods=['POST']) +def stripe_webhook(): + """ + Webhook for handling stripe payments + """ + event = None + payload = request.data + sig_header = request.headers['STRIPE_SIGNATURE'] + + try: + event = stripe.Webhook.construct_event( + payload, sig_header, endpoint_secret + ) + except ValueError as e: + raise e + except stripe.error.SignatureVerificationError as e: + raise e + + if event['type'] == "checkout.session.completed": + session = event['data']['object'] + product_id = stripe.checkout.Session.list_line_items(session['id'])['data'][0]['price']['product'] + if product_id == subscription: + client_reference_id = session.get("client_reference_id") + user_id, streamer_id = client_reference_id.split("-") + print(f"user_id: {user_id} is subscribing to streamer_id: {streamer_id}", flush=True) + subscribe(user_id, streamer_id) + + return "Success", 200 \ No newline at end of file diff --git a/web_server/blueprints/user.py b/web_server/blueprints/user.py index 672e4f2..2f6c4ca 100644 --- a/web_server/blueprints/user.py +++ b/web_server/blueprints/user.py @@ -22,17 +22,6 @@ def user_data(username: str): return jsonify(data) ## Subscription Routes -@login_required -@user_bp.route('/user/subscribe/') -def user_subscribe(streamer_id): - """ - Given a streamer subscribes as user - """ - #TODO: Keep this route secure so only webhooks from Stripe payment can trigger it - user_id = session.get("user_id") - subscribe(user_id, streamer_id) - return jsonify({"status": True}) - @login_required @user_bp.route('/user/subscription/') def user_subscribed(subscribed_id: int): diff --git a/web_server/utils/user_utils.py b/web_server/utils/user_utils.py index 91de868..420e36c 100644 --- a/web_server/utils/user_utils.py +++ b/web_server/utils/user_utils.py @@ -52,21 +52,18 @@ def is_user_partner(user_id: int) -> bool: return bool(data) def is_subscribed(user_id: int, subscribed_to_id: int) -> bool: - """ - Returns True if user is subscribed to a streamer, else False - """ + """Returns True if user is subscribed to a streamer, else False""" with Database() as db: - result = db.fetchone(""" - SELECT * + return bool(db.fetchone( + """ + SELECT 1 FROM subscribes WHERE user_id = ? - AND subscribed_id = ? + AND subscribed_id = ? AND expires > ?; - """, (user_id, subscribed_to_id, datetime.now())) - print(result) - if result: - return True - return False + """, + (user_id, subscribed_to_id, datetime.now()) + )) def is_following(user_id: int, followed_id: int) -> bool: """