πŸ’³ Billing Components

Presentation-only billing UI components for post-login transactional pages

🎯 Overview

Category Component Purpose
πŸ›’ Checkout CheckoutCard Post-login checkout with monthly/yearly toggle and POST form
⏳ Trial TrialBanner Dashboard banner showing trial status (info/warning tone)
πŸ“Š Status BillingStatusCard Subscription status card with configurable messaging

πŸ—οΈ Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 Post-Login Dashboard                     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  TrialBanner (info or warning tone)                     β”‚
β”‚  β”œβ”€ Days remaining + optional CTA                       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  BillingStatusCard                                      β”‚
β”‚  β”œβ”€ Plan label + status chip + period end + manage CTA  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  CheckoutCard                                           β”‚
β”‚  β”œβ”€ Monthly/yearly toggle + savings chip                β”‚
β”‚  β”œβ”€ POST form with hidden fields + CTA                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“‹ When to Use Which Component

Scenario Component Module
Public marketing page with multiple pricing tiers PricingSection web_pages
Post-login checkout for a single plan CheckoutCard billing
Dashboard banner showing trial countdown TrialBanner billing
Dashboard card showing subscription state BillingStatusCard billing

PricingSection is for pre-login marketing. The billing components are for post-login transactional UI. All billing components are presentation-only and backend-agnostic β€” no payment provider integration, no hardcoded product names.

Code
import socket
import time
import subprocess
from fastcore.utils import partial
from fasthtml.jupyter import FastHTML, JupyUvi, HTMX

def kill_process_on_port(port):
    """Kill any process using the specified port on Windows"""
    try:
        result = subprocess.run(
            f'netstat -ano | findstr :{port}',
            shell=True, capture_output=True, text=True
        )
        if result.stdout:
            lines = result.stdout.strip().split('\n')
            for line in lines:
                if 'LISTENING' in line:
                    pid = line.strip().split()[-1]
                    subprocess.run(f'taskkill /PID {pid} /F', shell=True, capture_output=True)
                    print(f"Killed process {pid} on port {port}")
                    time.sleep(0.5)
                    return True
        return False
    except Exception as e:
        print(f"Could not kill process on port {port}: {e}")
        return False

def find_available_port(start_port=5000, max_attempts=10):
    for port in range(start_port, start_port + max_attempts):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            try:
                s.bind(('', port))
                return port
            except OSError:
                continue
    raise RuntimeError(f"Could not find available port in range {start_port}-{start_port+max_attempts}")

if 'server' in globals():
    try:
        server.stop()
        time.sleep(0.5)
    except:
        pass

preferred_port = 7030
kill_process_on_port(preferred_port)
port = find_available_port(preferred_port)

app = FastHTML(hdrs=MatTheme.blue.headers(title="Billing Components", mode="dark"))
rt = app.route

try:
    server = JupyUvi(app, port=port)
    preview = partial(HTMX, app=app, port=port)
    print(f"Server running on port {port}")
except Exception as e:
    print(f"Failed to start server: {e}")
    raise
Server running on port 7030

πŸ›’ CheckoutCard

Component Purpose
CheckoutCard Single-plan checkout card with monthly/yearly toggle and POST form

Features: Instance-safe JS IDs (uuid), configurable copy, hidden form fields, savings chip, trial callout


source

CheckoutCard


def CheckoutCard(
    monthly_price:float, # Monthly price (required)
    yearly_price:float, # Yearly price (required)
    title:str='Choose Your Plan', # Card title
    subtitle:str='', # Card subtitle
    currency:str='$', # Currency symbol
    trial_days:int=0, # Trial period in days (0 = none)
    cta_text:str='Subscribe', # Submit button text
    form_action:str='/checkout', # POST form action URL
    plan_id:str='', # Hidden field: plan identifier
    hidden_fields:dict=None, # Additional hidden fields {name: value}
    default_period:str='monthly', # Default toggle: 'monthly' or 'yearly'
    fine_print:str='', # Small text below CTA
    cls:str='', # Additional CSS classes
    kwargs:VAR_KEYWORD
):

Single-plan checkout card with monthly/yearly toggle and POST form.

Designed for post-login checkout flows. Submits via POST form with hidden fields (plan_id, billing_period, plus any custom fields). All JS/CSS IDs are namespaced per instance using a uuid, so multiple CheckoutCards can coexist on one page.

Args: monthly_price: Monthly price as float (e.g., 9.99) yearly_price: Yearly price as float (e.g., 99.99) title: Card heading subtitle: Card subheading currency: Currency symbol prepended to prices trial_days: If > 0, shows a trial callout above the form cta_text: Text for the submit button form_action: URL the form POSTs to plan_id: Value for the hidden plan_id field hidden_fields: Dict of additional hidden form fields {name: value} default_period: Which period is selected on load (β€˜monthly’ or β€˜yearly’) fine_print: Small disclaimer text below the button cls: Additional CSS classes for the card

Example: CheckoutCard( monthly_price=9.99, yearly_price=99.99, plan_id=β€œpro”, trial_days=14, fine_print=β€œCancel anytime. No questions asked.”, )

Code
# Example: CheckoutCard with trial period and custom hidden fields
def ex_checkout_card():
    return Div(
        CheckoutCard(
            monthly_price=9.99,
            yearly_price=99.99,
            plan_id="pro",
            trial_days=14,
            hidden_fields={"coupon": "WELCOME10"},
            fine_print="Cancel anytime. No questions asked.",
        ),
        cls="responsive padding",
    )

preview(ex_checkout_card())
Code
# Example: CheckoutCard defaulting to yearly billing
def ex_checkout_yearly():
    return Div(
        CheckoutCard(
            monthly_price=29.99,
            yearly_price=249.99,
            title="Enterprise Plan",
            subtitle="For growing teams",
            default_period="yearly",
            cta_text="Start Free Trial",
            form_action="/billing/subscribe",
            plan_id="enterprise",
        ),
        cls="responsive padding",
    )

preview(ex_checkout_yearly())

⏳ TrialBanner

Component Purpose
TrialBanner Thin, dismissible top-of-page bar showing trial countdown

Features: Auto-derives info/warning tone, sits above the navbar, close button, configurable copy, optional CTA

Positioning: Place the banner before the navbar in the DOM. In a TopLayout route, return it as the first tuple element: (TrialBanner(...), *TopLayout(content, nav_bar=...))


source

TrialBanner


def TrialBanner(
    days_remaining:int, # Days left in trial
    end_date_text:str='', # e.g., 'Trial ends Mar 15'
    status:str='trialing', # Status key (for custom styling hooks)
    cta_text:str='', # Optional CTA link text
    cta_href:str='', # CTA link URL
    message:str='', # Override default message
    dismissible:bool=True, # Show close button
    banner_id:str='', # Custom element ID (auto-generated if empty)
    cls:str='', # Additional CSS classes
    kwargs:VAR_KEYWORD
):

Thin, dismissible trial banner β€” sits above the app layout.

Renders a compact full-width bar with icon, message, optional CTA, and close button. Place it before the Layout in the DOM so it appears above everything. When dismissed, it removes itself from the page.

Automatically switches to warning tone (error-container) when 3 or fewer days remain, otherwise uses info tone (primary-container).

Positioning with Layout::

@rt("/dashboard")
def get(req):
    content = dashboard_content()
    if 'HX-Request' in req.headers: return content
    return (
        TrialBanner(days_remaining=5, cta_text="Upgrade", cta_href="/checkout"),
        Layout(content, sidebar_links=my_sidebar_links()),
    )

Args: days_remaining: Number of days left in the trial end_date_text: Optional text (shown after a separator) status: Status string (available as a data attribute hook) cta_text: If non-empty, renders an inline CTA link cta_href: URL for the CTA link message: Custom message (overrides the default) dismissible: Whether to show the close button (default True) banner_id: Element ID for the banner (auto-generated if empty) cls: Additional CSS classes

Example: TrialBanner(days_remaining=14, cta_text=β€˜Upgrade Now’, cta_href=β€˜/checkout’) TrialBanner(days_remaining=2, end_date_text=β€˜Trial ends Feb 23’)

Code
# Example: TrialBanner in info tone (14 days remaining)
preview(TrialBanner(
    days_remaining=14,
    end_date_text="Trial ends March 15, 2026",
    cta_text="Upgrade Now",
    cta_href="/checkout",
))
Code
# Example: TrialBanner in warning tone (2 days remaining, non-dismissible)
preview(TrialBanner(
    days_remaining=2,
    end_date_text="Trial ends February 23, 2026",
    cta_text="Upgrade Now",
    cta_href="/checkout",
    dismissible=False,
))

πŸ“Š BillingStatusCard

Component Purpose
BillingStatusCard Subscription status card with status-specific messaging

Features: Status chip with semantic colors, configurable messages, optional manage CTA


source

BillingStatusCard


def BillingStatusCard(
    status:str, # 'active', 'trialing', 'past_due', 'canceled', 'checkout_pending'
    plan_label:str='', # e.g., 'Pro Plan'
    current_period_end:str='', # e.g., 'March 15, 2026'
    manage_url:str='', # URL for billing management portal
    manage_text:str='Manage Billing', # CTA button text
    messages:dict=None, # Override status -> message mapping
    cls:str='', # Additional CSS classes
    kwargs:VAR_KEYWORD
):

Subscription status card with status-specific messaging.

Displays the current subscription status with a semantic color chip, plan label, period end date, and an optional β€œManage Billing” CTA. All copy is configurable via the messages dict override.

Supported statuses: active, trialing, past_due, canceled, checkout_pending. Unknown statuses render with a neutral chip and the status string as message.

Args: status: Subscription status string plan_label: Name of the current plan current_period_end: Human-readable period end date manage_url: If non-empty, renders a manage billing button manage_text: Text for the manage billing button messages: Dict overriding default status messages cls: Additional CSS classes

Example: BillingStatusCard(status=β€˜active’, plan_label=β€˜Pro Plan’, current_period_end=β€˜March 15, 2026’, manage_url=β€˜/billing/portal’)

Code
# Example: BillingStatusCard for each status
def ex_billing_status_all():
    statuses = [
        ("active", "Pro Plan", "March 15, 2026"),
        ("trialing", "Pro Plan", "March 1, 2026"),
        ("past_due", "Pro Plan", "February 15, 2026"),
        ("canceled", "Pro Plan", ""),
        ("checkout_pending", "", ""),
    ]
    cards = [
        BillingStatusCard(
            status=s,
            plan_label=label,
            current_period_end=end,
            manage_url="/billing/portal" if s in ("active", "past_due") else "",
        )
        for s, label, end in statuses
    ]
    return Div(*cards, cls="responsive padding column medium-space")

preview(ex_billing_status_all())

🌐 Live App Preview

Full billing dashboard inside a Layout shell (sidebar + NavBar) with the TrialBanner sitting above the entire layout. After running the server cell and this cell, open the port URL:

  • /billing β€” Dashboard with TrialBanner + BillingStatusCard + CheckoutCard
  • /billing/checkout β€” Centered CheckoutCard with trial callout
  • /billing/status β€” All 5 subscription status cards + warning TrialBanner
Code
# --- Navigation items ---
def billing_nav_items():
    """Top NavBar links β€” hx-boost on Layout handles HTMX automatically."""
    return [
        A("Dashboard", href="/billing"),
        A("Checkout", href="/billing/checkout"),
        A("Status", href="/billing/status"),
    ]

def billing_sidebar_items():
    """Sidebar links with icons β€” hx-boost on Layout handles HTMX automatically."""
    return [
        A(Icon("dashboard"), Span("Dashboard"), href="/billing", cls="nav-link"),
        A(Icon("shopping_cart"), Span("Checkout"), href="/billing/checkout", cls="nav-link"),
        A(Icon("receipt_long"), Span("Status"), href="/billing/status", cls="nav-link"),
    ]

# Highlights the active sidebar link after each HTMX navigation
active_nav_script = Script("""
function updateActiveNav() {
    document.querySelectorAll('.nav-link').forEach(link => {
        link.classList.toggle('active', link.getAttribute('href') === window.location.pathname);
    });
}
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', updateActiveNav);
} else {
    updateActiveNav();
}
document.body.addEventListener('htmx:afterSettle', updateActiveNav);
""")


# --- App shell: TrialBanner sits above the full Layout ---
def get_billing_layout(content, days_remaining=5):
    banner = TrialBanner(
        days_remaining=days_remaining,
        end_date_text="Trial ends February 26, 2026",
        cta_text="Upgrade Now",
        cta_href="/billing/checkout",
    )
    return Div(
        banner,
        Layout(
            content,
            sidebar_links=billing_sidebar_items(),
            nav_bar=NavBar(*billing_nav_items(), brand=H3("BillingDemo"), sticky=True),
        ),
        active_nav_script,
    )


# --- Page content builders ---
def billing_dashboard_content():
    """Main billing dashboard: status card + checkout side-by-side."""
    return Div(
        Grid(
            GridCell(
                BillingStatusCard(
                    status="trialing",
                    plan_label="Pro Plan",
                    current_period_end="February 26, 2026",
                    manage_url="/billing/status",
                ),
                span="s12 m6",
            ),
            GridCell(
                CheckoutCard(
                    monthly_price=19.99,
                    yearly_price=199.99,
                    plan_id="pro",
                    trial_days=5,
                    cta_text="Subscribe Now",
                    form_action="/billing/checkout",
                ),
                span="s12 m6",
            ),
        ),
        cls="medium-space",
    )

def billing_checkout_content():
    """Dedicated checkout page with a single centered CheckoutCard."""
    return Grid(
        GridCell(span="s0 m2 l3"),
        GridCell(
            H3("Complete Your Purchase"),
            CheckoutCard(
                monthly_price=9.99,
                yearly_price=99.99,
                plan_id="pro",
                trial_days=14,
                hidden_fields={"coupon": "WELCOME10"},
                fine_print="Cancel anytime. No questions asked.",
                form_action="/billing/checkout",
            ),
            span="s12 m8 l6",
        ),
        GridCell(span="s0 m2 l3"),
    )

def billing_status_content():
    """All five subscription states for visual QA."""
    statuses = [
        ("active",           "Pro Plan", "March 15, 2026"),
        ("trialing",         "Pro Plan", "March 1, 2026"),
        ("past_due",         "Pro Plan", "February 15, 2026"),
        ("canceled",         "Pro Plan", ""),
        ("checkout_pending", "",         ""),
    ]
    cards = [
        BillingStatusCard(
            status=s, plan_label=label, current_period_end=end,
            manage_url="/billing" if s in ("active", "past_due") else "",
        )
        for s, label, end in statuses
    ]
    return Div(
        H3("All Subscription States"),
        Div(*cards, cls="medium-space"),
        cls="medium-space",
    )


# --- Routes ---
@rt("/billing")
def get(req):
    content = billing_dashboard_content()
    if "HX-Request" in req.headers: return content
    return get_billing_layout(content)

@rt("/billing/checkout")
def get(req):
    content = billing_checkout_content()
    if "HX-Request" in req.headers: return content
    return get_billing_layout(content)

@rt("/billing/status")
def get(req):
    content = billing_status_content()
    if "HX-Request" in req.headers: return content
    return get_billing_layout(content, days_remaining=2)  # warning tone on status page


# In-notebook preview
preview(get_billing_layout(billing_dashboard_content()))

πŸ” Mobile & Theme Verification

All billing components use BeerCSS semantic classes (primary-container, error-container, surface-container, etc.) so they automatically adapt to light/dark mode.

Manual Checks

  1. Mobile (320–480px): All cards stack full-width, toggle buttons remain tappable, form is usable
  2. Light mode: Remove dark class from <body> β€” verify contrast and readability
  3. Dark mode: Add dark class to <body> β€” verify all semantic colors adapt
  4. Form payload: Open browser dev tools β†’ Network tab β†’ submit form β†’ verify plan_id, billing_period, and custom hidden fields in POST body