FEAT: Added Stripe webhook to handle user subscriptions
UPDATE: Improved the chat look
This commit is contained in:
BIN
frontend/public/images/pfp/monkey.png
Normal file
BIN
frontend/public/images/pfp/monkey.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
@@ -21,9 +21,11 @@ export const Return: React.FC = () => {
|
|||||||
const sessionId = urlParams.get("session_id");
|
const sessionId = urlParams.get("session_id");
|
||||||
|
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
|
console.log("1")
|
||||||
fetch(`/api/session-status?session_id=${sessionId}`)
|
fetch(`/api/session-status?session_id=${sessionId}`)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
console.log("Response Data:", data);
|
||||||
setStatus(data.status);
|
setStatus(data.status);
|
||||||
setCustomerEmail(data.customer_email);
|
setCustomerEmail(data.customer_email);
|
||||||
});
|
});
|
||||||
@@ -54,9 +56,9 @@ interface CheckoutFormProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckoutForm: React.FC<CheckoutFormProps> = ({ onClose }) => {
|
const CheckoutForm: React.FC<CheckoutFormProps> = ({ onClose, streamerID }) => {
|
||||||
const fetchClientSecret = () => {
|
const fetchClientSecret = () => {
|
||||||
return fetch(`/api/create-checkout-session`, {
|
return fetch(`/api/create-checkout-session?streamer_id=${streamerID}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
|
|||||||
@@ -117,32 +117,48 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
|
|||||||
className="max-w-[30vw] h-full flex flex-col rounded-lg p-4"
|
className="max-w-[30vw] h-full flex flex-col rounded-lg p-4"
|
||||||
style={{ gridArea: "1 / 2 / 3 / 3" }}
|
style={{ gridArea: "1 / 2 / 3 / 3" }}
|
||||||
>
|
>
|
||||||
<h2 className="text-xl font-bold mb-4 text-white">Stream Chat</h2>
|
<h2 className="text-xl font-bold mb-4 text-white text-center">Stream Chat</h2>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={chatContainerRef}
|
ref={chatContainerRef}
|
||||||
id="chat-message-list"
|
id="chat-message-list"
|
||||||
className="flex-grow w-full max-h-[50vh] overflow-y-auto mb-4 space-y-2 rounded-md"
|
className="flex-grow w-full max-h-[30em] overflow-y-auto mb-4 space-y-2 rounded-md"
|
||||||
>
|
>
|
||||||
{messages.map((msg, index) => (
|
{messages.map((msg, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="grid grid-cols-[minmax(15%,_100px)_1fr] group h-fit items-center bg-gray-700 rounded p-2 text-white"
|
className="flex items-start space-x-2 bg-gray-800 rounded p-2 text-white"
|
||||||
>
|
>
|
||||||
<span
|
{/* User avatar with image */}
|
||||||
className={`font-bold ${
|
<div className="w-2em h-2em rounded-full overflow-hidden flex-shrink-0">
|
||||||
msg.chatter_username === username
|
<img
|
||||||
? "text-blue-400"
|
src="/images/monkey.png"
|
||||||
: "text-green-400"
|
alt="User Avatar"
|
||||||
}`}
|
className="w-full h-full object-cover"
|
||||||
>
|
style={{ width: '2.5em', height: '2.5em' }}
|
||||||
{" "}
|
/>
|
||||||
{msg.chatter_username}:{" "}
|
</div>
|
||||||
</span>
|
|
||||||
<span className="text-center">{msg.message}</span>
|
<div className="flex-grow">
|
||||||
<span className="text-gray-400 text-sm scale-0 group-hover:scale-100 h-[0px] group-hover:h-[10px] transition-all delay-1000 group-hover:delay-200">
|
<div className="flex items-center space-x-0.5em">
|
||||||
|
{/* Username */}
|
||||||
|
<span
|
||||||
|
className={`font-bold text-[1em] ${msg.chatter_username === username
|
||||||
|
? "text-purple-600"
|
||||||
|
: "text-green-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{msg.chatter_username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Message content */}
|
||||||
|
<div className="text-[0.9em] mt-0.5em">{msg.message}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Time sent */}
|
||||||
|
<div className="text-gray-500 text-[0.8em] self-start">
|
||||||
{new Date(msg.time_sent).toLocaleTimeString()}
|
{new Date(msg.time_sent).toLocaleTimeString()}
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamerId }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{showCheckout && <CheckoutForm onClose={() => setShowCheckout(false)} />}
|
{showCheckout && <CheckoutForm onClose={() => setShowCheckout(false)} streamerID={streamerId}/>}
|
||||||
{showReturn && <Return />}
|
{showReturn && <Return />}
|
||||||
{showAuthModal && <AuthModal onClose={() => setShowAuthModal(false)} />}
|
{showAuthModal && <AuthModal onClose={() => setShowAuthModal(false)} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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
|
import os, stripe
|
||||||
|
|
||||||
stripe_bp = Blueprint("stripe", __name__)
|
stripe_bp = Blueprint("stripe", __name__)
|
||||||
|
|
||||||
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
|
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'])
|
@stripe_bp.route('/create-checkout-session', methods=['POST'])
|
||||||
def create_checkout_session():
|
def create_checkout_session():
|
||||||
"""
|
"""
|
||||||
Creates the stripe checkout session
|
Creates the stripe checkout session
|
||||||
"""
|
"""
|
||||||
print("Creating checkout session")
|
print("Creating checkout session", flush=True)
|
||||||
try:
|
try:
|
||||||
|
user_id = s.get("user_id")
|
||||||
|
streamer_id = request.args.get("streamer_id")
|
||||||
session = stripe.checkout.Session.create(
|
session = stripe.checkout.Session.create(
|
||||||
ui_mode = 'embedded',
|
ui_mode = 'embedded',
|
||||||
payment_method_types=['card'],
|
payment_method_types=['card'],
|
||||||
line_items=[
|
line_items=[
|
||||||
{
|
{
|
||||||
'price': 'price_1QikNCGk6yuk3uA86mZf3dmM', #Subscription ID
|
'price': 'price_1QikNCGk6yuk3uA86mZf3dmM',
|
||||||
'quantity': 1,
|
'quantity': 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
mode='subscription',
|
mode='subscription',
|
||||||
redirect_on_completion = 'never'
|
redirect_on_completion = 'never',
|
||||||
|
client_reference_id = f"{user_id}-{streamer_id}"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e, flush=True)
|
||||||
return str(e)
|
return str(e), 500
|
||||||
|
|
||||||
return jsonify(clientSecret=session.client_secret)
|
return jsonify(clientSecret=session.client_secret)
|
||||||
|
|
||||||
@@ -38,3 +47,32 @@ def session_status():
|
|||||||
session = stripe.checkout.Session.retrieve(request.args.get('session_id'))
|
session = stripe.checkout.Session.retrieve(request.args.get('session_id'))
|
||||||
|
|
||||||
return jsonify(status=session.status, customer_email=session.customer_details.email)
|
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
|
||||||
@@ -22,17 +22,6 @@ def user_data(username: str):
|
|||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
||||||
## Subscription Routes
|
## Subscription Routes
|
||||||
@login_required
|
|
||||||
@user_bp.route('/user/subscribe/<int:streamer_id>')
|
|
||||||
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
|
@login_required
|
||||||
@user_bp.route('/user/subscription/<int:subscribed_id>')
|
@user_bp.route('/user/subscription/<int:subscribed_id>')
|
||||||
def user_subscribed(subscribed_id: int):
|
def user_subscribed(subscribed_id: int):
|
||||||
|
|||||||
@@ -52,21 +52,18 @@ def is_user_partner(user_id: int) -> bool:
|
|||||||
return bool(data)
|
return bool(data)
|
||||||
|
|
||||||
def is_subscribed(user_id: int, subscribed_to_id: int) -> bool:
|
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:
|
with Database() as db:
|
||||||
result = db.fetchone("""
|
return bool(db.fetchone(
|
||||||
SELECT *
|
"""
|
||||||
|
SELECT 1
|
||||||
FROM subscribes
|
FROM subscribes
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
AND subscribed_id = ?
|
AND subscribed_id = ?
|
||||||
AND expires > ?;
|
AND expires > ?;
|
||||||
""", (user_id, subscribed_to_id, datetime.now()))
|
""",
|
||||||
print(result)
|
(user_id, subscribed_to_id, datetime.now())
|
||||||
if result:
|
))
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_following(user_id: int, followed_id: int) -> bool:
|
def is_following(user_id: int, followed_id: int) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user