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)