summaryrefslogtreecommitdiff
path: root/Biz
diff options
context:
space:
mode:
authorBen Sima <ben@bsima.me>2025-11-20 18:07:12 -0500
committerBen Sima <ben@bsima.me>2025-11-20 18:07:12 -0500
commit028bbda4282515b95a7555209d397aaf22d32244 (patch)
tree38c5527a546dcb1f877f3380f615ae4497faf50f /Biz
parent6e90d59acf45cc481e4e78101a36231af43cbd96 (diff)
feat: implement t-PpYZt2
Diffstat (limited to 'Biz')
-rw-r--r--Biz/PodcastItLater/UI.py162
-rw-r--r--Biz/PodcastItLater/Web.py30
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