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:
208
server.py
208
server.py
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user