diff options
| author | Ben Sima <ben@bsima.me> | 2025-11-20 19:25:01 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bsima.me> | 2025-11-20 19:25:01 -0500 |
| commit | 84251fc367e0d129dd77e951f587e2e2db0e98f6 (patch) | |
| tree | 4a23c5c246b96b8a99727304165c00433c372d5c /Biz/PodcastItLater | |
| parent | 0f3d43b40b39c8915303ee19901638197c33f1c6 (diff) | |
| parent | ada597842025a4dd083dcaf0f278b1123447c760 (diff) | |
Merge branch 'task/t-PpYZt2' into live
Diffstat (limited to 'Biz/PodcastItLater')
| -rw-r--r-- | Biz/PodcastItLater/UI.py | 162 | ||||
| -rw-r--r-- | Biz/PodcastItLater/Web.py | 30 |
2 files changed, 192 insertions, 0 deletions
diff --git a/Biz/PodcastItLater/UI.py b/Biz/PodcastItLater/UI.py index da0193c..27f5fff 100644 --- a/Biz/PodcastItLater/UI.py +++ b/Biz/PodcastItLater/UI.py @@ -215,6 +215,24 @@ class PageLayout(Component[AnyChildren, PageLayoutAttrs]): html.i( classes=[ "bi", + "bi-stars", + "me-1", + ], + ), + "Pricing", + href="/pricing", + classes=[ + "nav-link", + "active" if is_active("pricing") else "", + ], + ), + classes=["nav-item"], + ), + html.li( + html.a( + html.i( + classes=[ + "bi", "bi-person-circle", "me-1", ], @@ -387,3 +405,147 @@ class PageLayout(Component[AnyChildren, PageLayoutAttrs]): create_bootstrap_js(), ), ) + + +class PricingPageAttrs(Attrs): + """Attributes for PricingPage component.""" + + user: dict[str, typing.Any] | None + + +class PricingPage(Component[AnyChildren, PricingPageAttrs]): + """Pricing page component.""" + + @override + def render(self) -> PageLayout: + user = self.attrs.get("user") + current_tier = user.get("plan_tier", "free") if user else "free" + + return PageLayout( + user=user, + current_page="pricing", + page_title="Pricing - PodcastItLater", + error=None, + meta_tags=[], + children=[ + html.div( + html.h2("Simple Pricing", classes=["text-center", "mb-5"]), + html.div( + # Free Tier + html.div( + html.div( + html.div( + html.h3("Free", classes=["card-title"]), + html.h4( + "$0", + classes=[ + "card-subtitle", + "mb-3", + "text-muted", + ], + ), + html.p( + "10 articles total", + classes=["card-text"], + ), + html.ul( + html.li("Convert 10 articles"), + html.li("Basic features"), + classes=["list-unstyled", "mb-4"], + ), + html.button( + "Current Plan", + classes=[ + "btn", + "btn-outline-primary", + "w-100", + ], + disabled=True, + ) + if current_tier == "free" + else html.div(), + classes=["card-body"], + ), + classes=["card", "mb-4", "shadow-sm", "h-100"], + ), + classes=["col-md-6"], + ), + # Paid Tier + html.div( + html.div( + html.div( + html.h3( + "Unlimited", + classes=["card-title"], + ), + html.h4( + "$12/mo", + classes=[ + "card-subtitle", + "mb-3", + "text-muted", + ], + ), + html.p( + "Unlimited articles", + classes=["card-text"], + ), + html.ul( + html.li("Unlimited conversions"), + html.li("Priority processing"), + html.li("Support independent software"), + classes=["list-unstyled", "mb-4"], + ), + html.form( + html.button( + "Upgrade Now", + type="submit", + classes=[ + "btn", + "btn-primary", + "w-100", + ], + ), + action="/upgrade", + method="POST", + ) + if user and current_tier == "free" + else ( + html.button( + "Current Plan", + classes=[ + "btn", + "btn-success", + "w-100", + ], + disabled=True, + ) + if user and current_tier == "paid" + else html.a( + "Login to Upgrade", + href="/", + classes=[ + "btn", + "btn-primary", + "w-100", + ], + ) + ), + classes=["card-body"], + ), + classes=[ + "card", + "mb-4", + "shadow-sm", + "border-primary", + "h-100", + ], + ), + classes=["col-md-6"], + ), + classes=["row"], + ), + classes=["container", "py-3"], + ), + ], + ) diff --git a/Biz/PodcastItLater/Web.py b/Biz/PodcastItLater/Web.py index 4a8e57e..7e8e969 100644 --- a/Biz/PodcastItLater/Web.py +++ b/Biz/PodcastItLater/Web.py @@ -973,6 +973,36 @@ def public_feed(request: Request) -> PublicFeedPage: ) +@app.get("/pricing") +def pricing(request: Request) -> UI.PricingPage: + """Display pricing page.""" + user_id = request.session.get("user_id") + user = Core.Database.get_user_by_id(user_id) if user_id else None + + return UI.PricingPage( + user=user, + ) + + +@app.post("/upgrade") +def upgrade(request: Request) -> RedirectResponse: + """Start upgrade checkout flow.""" + user_id = request.session.get("user_id") + if not user_id: + return RedirectResponse(url="/?error=login_required") + + try: + checkout_url = Billing.create_checkout_session( + user_id, + "paid", + BASE_URL, + ) + return RedirectResponse(url=checkout_url, status_code=303) + except ValueError: + logger.exception("Failed to create checkout session") + return RedirectResponse(url="/pricing?error=checkout_failed") + + def _handle_test_login(email: str, request: Request) -> Response: """Handle login in test mode.""" # Special handling for demo account |
