Skip to content

Commit

Permalink
Implement creating a subscription, but no webhooks yet
Browse files Browse the repository at this point in the history
  • Loading branch information
micahflee committed Sep 6, 2024
1 parent a5051eb commit 4483261
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 7 deletions.
74 changes: 71 additions & 3 deletions hushline/premium.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,90 @@
from flask import Blueprint, redirect, render_template, session, url_for
from flask import (
Blueprint,
current_app,
flash,
redirect,
render_template,
request,
session,
url_for,
)
from werkzeug.wrappers.response import Response

from .db import db
from .model import User
from .model import Tier, User
from .stripe import create_subscription, get_latest_invoice_payment_intent_client_secret
from .utils import authentication_required

FREE_TIER = 1
BUSINESS_TIER = 2


def create_blueprint() -> Blueprint:
bp = Blueprint("premium", __file__, url_prefix="/premium")

@bp.route("/", methods=["GET"])
@authentication_required
def premium() -> Response | str:
def index() -> Response | str:
user = db.session.get(User, session.get("user_id"))
if not user:
session.clear()
return redirect(url_for("login"))

return render_template("premium.html", user=user)

@bp.route("/upgrade", methods=["GET", "POST"])
@authentication_required
def upgrade() -> Response | str:
if request.method == "GET":
return redirect(url_for("premium.index"))

user = db.session.get(User, session.get("user_id"))
if not user:
session.clear()
return redirect(url_for("login"))

# If the user is already on the business tier
if user.tier_id == BUSINESS_TIER:
flash("👍 You're already upgraded.")
return redirect(url_for("premium.index"))

# Select the business tier
business_tier = db.session.query(Tier).get(BUSINESS_TIER)
if not business_tier:
flash("⚠️ Something went wrong!")
return redirect(url_for("premium.index"))

# Subscribe the user to the business tier
try:
stripe_subscription = create_subscription(user, business_tier)
except Exception as e:
current_app.logger.error(f"Stripe error: {e}")
flash("⚠️ Something went wrong!")
return redirect(url_for("premium.index"))

return render_template(
"premium_subscribe.html",
user=user,
tier=business_tier,
stripe_subscription_id=stripe_subscription.id,
stripe_client_secret=get_latest_invoice_payment_intent_client_secret(
stripe_subscription
),
stripe_publishable_key=current_app.config.get("STRIPE_PUBLISHABLE_KEY"),
)

@bp.route("/downgrade", methods=["POST"])
@authentication_required
def downgrade() -> Response:
user = db.session.get(User, session.get("user_id"))
if not user:
session.clear()
return redirect(url_for("login"))

# user.premium = False
# db.session.add(user)
# db.session.commit()

return redirect(url_for("premium.index"))

return bp
6 changes: 6 additions & 0 deletions hushline/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2055,3 +2055,9 @@ p.bio + .extra-fields {
.plan-recommended .plan-status {
font-weight: bold;
}

#card-element {
margin-bottom: 1rem;
padding: 1rem;
border: 1px solid #666;
}
37 changes: 37 additions & 0 deletions hushline/static/js/premium-subscribe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
document.addEventListener("DOMContentLoaded", async function () {
const stripeClientSecret = document.querySelector(
"input[name='stripe_client_secret']",
).value;
const stripePublishableKey = document.querySelector(
"input[name='stripe_publishable_key']",
).value;
const pathPrefix = window.location.pathname.split("/").slice(0, -1).join("/");

stripe = Stripe(stripePublishableKey);
const elements = stripe.elements();
const cardElement = elements.create("card");
cardElement.mount("#card-element");

const form = document.querySelector("#subscribe-form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const nameInput = document.getElementById("name");

// Create payment method and confirm payment intent
const result = await stripe.confirmCardPayment(stripeClientSecret, {
payment_method: {
card: cardElement,
billing_details: {
name: nameInput.value,
},
},
});

if (result.error) {
alert(`Payment failed: ${result.error.message}`);
return;
} else {
window.location.href = pathPrefix;
}
});
});
Empty file removed hushline/static/js/premium.js
Empty file.
45 changes: 44 additions & 1 deletion hushline/stripe.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from flask import current_app

from .db import db
from .model import Tier
from .model import Tier, User


def init_stripe() -> None:
Expand Down Expand Up @@ -86,3 +86,46 @@ def update_price(tier: Tier) -> None:
db.session.commit()

stripe.Product.modify(tier.stripe_product_id, default_price=price.id)


def create_customer(user: User) -> stripe.Customer:
email: str = user.email if user.email is not None else ""

if user.stripe_customer_id is None:
stripe_customer = stripe.Customer.create(email=email)
user.stripe_customer_id = stripe_customer.id
db.session.add(user)
db.session.commit()
return stripe_customer

return stripe.Customer.modify(user.stripe_customer_id, email=email)


def create_subscription(user: User, tier: Tier) -> stripe.Subscription:
stripe_customer = create_customer(user)

# Create a subscription
stripe_subscription = stripe.Subscription.create(
customer=stripe_customer.id,
items=[{"price": tier.stripe_price_id}],
payment_behavior="default_incomplete",
)
user.stripe_subscription_id = stripe_subscription.id
db.session.add(user)
db.session.commit()

return stripe_subscription


def get_latest_invoice_payment_intent_client_secret(
subscription: stripe.Subscription,
) -> str | None:
if subscription.latest_invoice is None:
return None

stripe_invoice = stripe.Invoice.retrieve(str(subscription.latest_invoice))
if stripe_invoice.payment_intent is None:
return None

stripe_payment_intent = stripe.PaymentIntent.retrieve(str(stripe_invoice.payment_intent))
return stripe_payment_intent.client_secret
9 changes: 6 additions & 3 deletions hushline/templates/premium.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ <h3>Free</h3>
<p>Tor Onion Service: ✅</p>
<p>End-to-End Encrypted: ✅</p>
{% if user.tier_id == 2 %}
<button id="subscribe">Downgrade</button>
<form action="{{ url_for('premium.downgrade') }}" method="post">
<button id="downgrade">Downgrade</button>
</form>
{% endif %}
</div>
<div class="plan plan-recommended">
Expand All @@ -31,12 +33,13 @@ <h3>Business</h3>
<p>Monthly Messages: Unlimited</p>
<p>Forwarding Addresses: 100</p>
{% if user.tier_id == 1 %}
<button id="subscribe">Upgrade for $20/mo</button>
<form action="{{ url_for('premium.upgrade') }}" method="post">
<button id="upgrade">Upgrade for $20/mo</button>
</form>
{% endif %}
</div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/premium.js') }}"></script>
{% endblock %}
31 changes: 31 additions & 0 deletions hushline/templates/premium_subscribe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block title %}Subscribe to {{ tier.name }}{% endblock %}
{% block content %}
<h2>Subscribe to {{ tier.name }}</h2>
<form id="subscribe-form">
<input
type="hidden"
name="stripe_client_secret"
value="{{ stripe_client_secret }}"
/>
<input
type="hidden"
name="stripe_publishable_key"
value="{{ stripe_publishable_key }}"
/>
<div>
<label for="name">Full Name</label>
<input type="text" id="name" placeholder="Enter your name" />
</div>
<div>
<label for="card-element">Credit Card Details</label>
<div id="card-element"></div>
</div>
<button id="submit">Subscribe</button>
</form>
{% endblock %}

{% block scripts %}
<script src="https://js.stripe.com/v3/"></script>
<script src="{{ url_for('static', filename='js/premium-subscribe.js') }}"></script>
{% endblock %}

0 comments on commit 4483261

Please sign in to comment.