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
This commit is contained in:
2025-10-07 17:22:51 +01:00
parent bfdcee8602
commit 3ddbc40bb5
2655 changed files with 516264 additions and 2 deletions

208
server.py
View File

@@ -5,7 +5,7 @@ import stripe
import json
from datetime import datetime
load_dotenv()
load_dotenv('stripe_config/.env')
app = Flask(__name__)
@@ -16,6 +16,46 @@ 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")
@@ -97,5 +137,171 @@ def get_supporters():
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)