Files
donate/server.py
ComputerTech312 3ddbc40bb5 Improve subscription UI with large tier buttons
- Replace dropdown tier selection with attractive visual buttons
- Add tier-button CSS with hover effects and selection states
- Remove 'or pay by card' divider from subscription form for cleaner UI
- Update JavaScript to handle tier button selection events
- Fix Stripe module import conflict by renaming stripe directory to stripe_config
- Add responsive grid layout for tier buttons on mobile devices
2025-10-07 17:22:51 +01:00

308 lines
10 KiB
Python

import os
from flask import Flask, request, jsonify, render_template
from dotenv import load_dotenv
import stripe
import json
from datetime import datetime
load_dotenv('stripe_config/.env')
app = Flask(__name__)
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
PUBLISHABLE_KEY = os.getenv("STRIPE_PUBLISHABLE_KEY")
DEFAULT_CURRENCY = os.getenv("DEFAULT_CURRENCY", "GBP")
# In-memory supporter storage (in production, use a proper database)
supporters = []
# Subscription tiers - UPDATE THESE WITH YOUR REAL STRIPE PRICE IDs
SUBSCRIPTION_TIERS = {
"bronze": {
"price_id": "price_1SFdA1H7p78X3gVbFafKga1f", # Bronze tier price ID from Stripe Dashboard
"amount": 2.00,
"currency": "GBP",
"interval": "month",
"name": "Bronze",
"description": "Essential support that makes a difference"
},
"silver": {
"price_id": "price_1SFdD4H7p78X3gVb0ENAfHNZ", # Silver tier price ID from Stripe Dashboard
"amount": 5.00,
"currency": "GBP",
"interval": "month",
"name": "Silver",
"description": "Enhanced support with greater impact"
},
"gold": {
"price_id": "price_1SFdFkH7p78X3gVbLfjZvKIW", # Gold tier price ID from Stripe Dashboard
"amount": 10.00,
"currency": "GBP",
"interval": "month",
"name": "Gold",
"description": "Premium support for maximum impact"
},
"diamond": {
"price_id": "price_1SFdHFH7p78X3gVbN7UdxkPa", # Diamond tier price ID from Stripe Dashboard
"amount": 25.00,
"currency": "GBP",
"interval": "month",
"name": "Diamond",
"description": "Ultimate support for our biggest champions"
}
}
# In-memory customer storage (in production, use a proper database)
customers = []
subscriptions = []
@app.route("/")
def index():
return render_template("index.html")
@app.route("/config")
def config():
return jsonify({"publishableKey": PUBLISHABLE_KEY})
@app.route("/create-payment-intent", methods=["POST"])
def create_payment():
data = request.get_json()
amount = int(float(data["amount"]) * 100) # convert to cents/pence
currency = data.get("currency", DEFAULT_CURRENCY).lower()
supporter_name = data.get("supporterName", "").strip()
intent = stripe.PaymentIntent.create(
amount=amount,
currency=currency,
automatic_payment_methods={
"enabled": True,
"allow_redirects": "never" # Keep users on the page
},
metadata={
"source": "donation_page",
"currency": currency,
"supporter_name": supporter_name if supporter_name else "Anonymous"
}
)
return jsonify({"clientSecret": intent.client_secret})
@app.route("/add-supporter", methods=["POST"])
def add_supporter():
data = request.get_json()
supporter_name = data.get("name", "Anonymous").strip()
amount = data.get("amount", 0)
currency = data.get("currency", DEFAULT_CURRENCY)
if supporter_name and supporter_name != "Anonymous":
# Limit name length and sanitize
supporter_name = supporter_name[:30]
supporter = {
"name": supporter_name,
"amount": amount,
"currency": currency,
"timestamp": datetime.now().isoformat(),
"time_ago": "just now"
}
# Add to beginning of list and keep only last 50
supporters.insert(0, supporter)
supporters[:] = supporters[:50]
return jsonify({"success": True})
return jsonify({"success": False})
@app.route("/supporters", methods=["GET"])
def get_supporters():
# Update time_ago for each supporter
now = datetime.now()
for supporter in supporters:
try:
timestamp = datetime.fromisoformat(supporter["timestamp"])
diff = now - timestamp
if diff.days > 0:
supporter["time_ago"] = f"{diff.days}d ago"
elif diff.seconds > 3600:
hours = diff.seconds // 3600
supporter["time_ago"] = f"{hours}h ago"
elif diff.seconds > 60:
minutes = diff.seconds // 60
supporter["time_ago"] = f"{minutes}m ago"
else:
supporter["time_ago"] = "just now"
except:
supporter["time_ago"] = "recently"
return jsonify({"supporters": supporters[:20]}) # Return last 20
@app.route("/subscription-tiers", methods=["GET"])
def get_subscription_tiers():
"""Get available subscription tiers"""
return jsonify({"tiers": SUBSCRIPTION_TIERS})
@app.route("/create-subscription", methods=["POST"])
def create_subscription():
"""Create a new subscription - NOTE: Requires real Stripe Price IDs"""
data = request.get_json()
tier_id = data.get("tier_id")
email = data.get("email")
name = data.get("name", "").strip()
if tier_id not in SUBSCRIPTION_TIERS:
return jsonify({"error": "Invalid subscription tier"}), 400
if not email:
return jsonify({"error": "Email is required for subscriptions"}), 400
# Check if price IDs are still placeholders
price_id = SUBSCRIPTION_TIERS[tier_id]["price_id"]
if price_id.startswith("price_1234567890"):
return jsonify({
"error": "Subscription functionality requires setting up real Stripe Price IDs. "
"Please create products and prices in your Stripe Dashboard first."
}), 400
try:
# Create or retrieve customer
customer = stripe.Customer.create(
email=email,
name=name if name else None,
metadata={
"source": "donation_subscription"
}
)
# Create subscription
subscription = stripe.Subscription.create(
customer=customer.id,
items=[{"price": price_id}],
payment_behavior="default_incomplete",
payment_settings={"save_default_payment_method": "on_subscription"},
expand=["latest_invoice.payment_intent"],
)
# Store customer and subscription data
customer_data = {
"id": customer.id,
"email": email,
"name": name,
"created": datetime.now().isoformat()
}
customers.append(customer_data)
subscription_data = {
"id": subscription.id,
"customer_id": customer.id,
"tier_id": tier_id,
"status": subscription.status,
"created": datetime.now().isoformat()
}
subscriptions.append(subscription_data)
return jsonify({
"subscriptionId": subscription.id,
"clientSecret": subscription.latest_invoice.payment_intent.client_secret
})
except Exception as e:
# Catch all exceptions since IDE might not recognize stripe error classes
error_msg = str(e)
if "No such price" in error_msg:
return jsonify({"error": "Invalid price ID. Please check your Stripe Dashboard."}), 400
return jsonify({"error": f"Subscription creation failed: {error_msg}"}), 400
@app.route("/cancel-subscription", methods=["POST"])
def cancel_subscription():
"""Cancel a subscription"""
data = request.get_json()
subscription_id = data.get("subscription_id")
if not subscription_id:
return jsonify({"error": "Subscription ID is required"}), 400
try:
# Cancel the subscription at period end
subscription = stripe.Subscription.modify(
subscription_id,
cancel_at_period_end=True
)
# Update local storage
for sub in subscriptions:
if sub["id"] == subscription_id:
sub["status"] = "cancel_at_period_end"
sub["cancelled_at"] = datetime.now().isoformat()
break
return jsonify({
"success": True,
"message": "Subscription will be cancelled at the end of the current billing period"
})
except Exception as e:
return jsonify({"error": f"Cancellation failed: {str(e)}"}), 400
@app.route("/webhook", methods=["POST"])
def stripe_webhook():
"""Handle Stripe webhooks"""
payload = request.data
sig_header = request.headers.get('Stripe-Signature')
endpoint_secret = os.getenv('STRIPE_WEBHOOK_SECRET')
if not endpoint_secret:
return jsonify({"error": "Webhook secret not configured"}), 400
try:
event = stripe.Webhook.construct_event(payload, sig_header, endpoint_secret)
except ValueError:
return jsonify({"error": "Invalid payload"}), 400
except Exception as signature_error:
if "signature" in str(signature_error).lower():
return jsonify({"error": "Invalid signature"}), 400
return jsonify({"error": "Webhook processing failed"}), 400
# Handle the event
if event['type'] == 'invoice.payment_succeeded':
subscription = event['data']['object']['subscription']
customer_id = event['data']['object']['customer']
# Update subscription status
for sub in subscriptions:
if sub["id"] == subscription:
sub["status"] = "active"
sub["last_payment"] = datetime.now().isoformat()
break
print(f"Payment succeeded for subscription {subscription}")
elif event['type'] == 'invoice.payment_failed':
subscription = event['data']['object']['subscription']
# Update subscription status
for sub in subscriptions:
if sub["id"] == subscription:
sub["status"] = "past_due"
sub["last_failed_payment"] = datetime.now().isoformat()
break
print(f"Payment failed for subscription {subscription}")
elif event['type'] == 'customer.subscription.deleted':
subscription_id = event['data']['object']['id']
# Update subscription status
for sub in subscriptions:
if sub["id"] == subscription_id:
sub["status"] = "cancelled"
sub["cancelled_at"] = datetime.now().isoformat()
break
print(f"Subscription {subscription_id} was cancelled")
return jsonify({"success": True})
if __name__ == "__main__":
app.run(port=4242, debug=True)