UseThatApp Logo
    Python/Dash

    Dash

    Sign users in with their UseThatApp account and read their live plan in a Dash app, over OAuth 2.0 / OpenID Connect. These routes wire your app into the whole selling loop: a buyer who purchases from your own website lands back here signed in, entitlement already live — and marketplace launches arrive the same way. A Dash app has an underlying Flask server (app.server): you add the /login, /callback, and /logout routes there, then read the entitlement inside a Dash callback (which runs with the Flask session context). For the conceptual model, see OpenID Connect.

    This guide covers the Hosted sign-in path

    It has UseThatApp run login, so buyers return signed in and the same session answers what they bought — the Hosted sign-in add-on. If your app already has its own accounts, you do not need any of the sign-in wiring below: keep your login and verify purchases with a license key your server validates in one call, included in the per-sale fee.

    How it works

    1. Routes on app.server — the OIDC flow is plain Flask: /login starts it, /callback finishes it, /logout ends the session. The usethatapp SDK runs on your server as a confidential client.
    2. Entitlement in a Dash callback — a Dash callback can read the Flask session, so it pulls the access token and calls get_entitlement to drive what the UI shows.

    Prerequisites

    • Python 3.9+ and a Dash app.
    • A Flask secret key on app.server so the session cookie survives the redirect to /callback.
    • UTA_CLIENT_ID, UTA_CLIENT_SECRET, and UTA_REDIRECT_URI in your server environment. Register the OAuth client and redirect URI on your app's Integration page on the UseThatApp dashboard.
    • Pointing at a non-production instance? Also set UTA_ISSUER and UTA_API_URL — each defaults to production.

    Step 1 — Install

    terminal
    pip install usethatapp dash

    Step 2 — App and Flask server

    Grab the underlying Flask server with app.server and give it a secret key. OAuth needs the session cookie to survive the top-level redirect back to /callback — SameSite=Lax is sufficient for that GET. The common local-dev gotcha is a host mismatch (127.0.0.1 vs localhost): the cookie is scoped to one host, so always browse the app on the same host as your UTA_REDIRECT_URI.

    app.py
    import os
    
    from dash import Dash, html, callback, Output, Input
    from flask import jsonify, redirect, request, session
    
    from usethatapp import (
        UtaError,
        UtaTokenError,
        begin_login,
        complete_login,
        get_entitlement,
        logout_url,
    )
    
    app = Dash(__name__)
    server = app.server
    server.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-only-change-me")
    server.config.update(SESSION_COOKIE_SAMESITE="Lax", SESSION_COOKIE_HTTPONLY=True)

    Step 3 — OIDC routes on the server

    Register /login, /callback, and /logout on server (the Flask app). A marketplace launch redirects the browser to /login; you can also hit it directly from a "Log in with UseThatApp" link.

    app.py
    @server.get("/login")
    def uta_login():
        auth_url, flow_state = begin_login()
        session["uta_flow"] = flow_state  # JSON-serializable; stash for the callback
        return redirect(auth_url, code=302)
    
    
    @server.get("/callback")
    def uta_callback():
        # On cancel/deny (or a provider rejection), OAuth redirects back here with
        # ?error=... and NO code. Handle it gracefully instead of failing.
        if request.args.get("error"):
            session.pop("uta_flow", None)
            return redirect("/", code=303)
    
        flow = session.pop("uta_flow", None)
        if not flow:
            # The cookie did not round-trip — usually a host mismatch. Browse the
            # app on the SAME host as UTA_REDIRECT_URI (e.g. http://localhost:8050,
            # not 127.0.0.1).
            return jsonify({"error": "session lost between /login and /callback"}), 400
        try:
            s = complete_login(
                code=request.args.get("code"),
                state=request.args.get("state"),
                flow_state=flow,
            )
        except UtaError as e:
            return jsonify({"error": str(e)}), 400
        session["uta_sub"] = s.sub
        session["uta_access_token"] = s.access_token
        session["uta_id_token"] = s.id_token
        return redirect("/", code=303)
    
    
    @server.get("/logout")
    def uta_logout():
        # Don't clear our session yet — the user may choose "Stay signed in" at
        # UseThatApp. Reconcile on return: a real logout revokes the token, so the
        # next entitlement call (in the Dash callback) returns 401 and clears it.
        id_token = session.get("uta_id_token")
        try:
            target = logout_url(
                id_token=id_token,
                post_logout_redirect_uri=request.host_url.rstrip("/") + "/",
            )
            return redirect(target, code=302)
        except UtaError:
            return redirect("/", code=302)

    Step 4 — Layout and links

    app.py
    app.layout = html.Div([
        html.H1("Your App Version"),
        html.Div([
            html.A("Log in with UseThatApp", href="/login", style={"marginRight": "12px"}),
            html.A("Log out", href="/logout"),
        ]),
        html.Div(id="version-display", children="…"),
        html.Button("Refresh Version", id="update-button", n_clicks=0),
    ])

    Step 5 — Read entitlement in a callback

    A Dash callback runs with the Flask request context, so it can read the session. No token means an anonymous visitor — in the OAuth model a free launch carries no login by design, so for an app with a free tier "no token" is the free experience. A UtaTokenError (401) means the token was revoked or expired (e.g. the user signed out of UseThatApp) — drop back to the anonymous experience.

    app.py
    @callback(
        Output("version-display", "children"),
        Input("update-button", "n_clicks"),
    )
    def display_version(_n_clicks):
        token = session.get("uta_access_token")
        if not token:
            # Anonymous — a free launch carries no login, so this IS the free tier.
            return "Free version (not signed in)"
        try:
            ent = get_entitlement(token)
        except UtaTokenError:
            # Token expired/revoked — reconcile and fall back to the free experience.
            for k in ("uta_access_token", "uta_sub", "uta_id_token"):
                session.pop(k, None)
            return "Free version — signed out (log in to see your plan)"
        except UtaError as e:
            return f"Error: {e}"
        # Gate features on the stable ent.product_public_id, not the mutable ent.version.
        state = "entitled" if ent.entitled else "not entitled"
        return f"{ent.version or 'free'} — {state} (status={ent.status})"

    Offer the upgrade: purchase links

    When the entitlement read says the user is not entitled — or is an anonymous free-tier visitor — send them to hosted checkout with purchase_url(). UseThatApp is the merchant of record (payment, tax, refunds), and the buyer comes back to your app signed in with the entitlement already live: no webhooks, no polling. purchase_url() and manage_url() are pure URL builders — no network call — so they're fine to call inside a Dash callback (or anywhere else server-side), and the CTA is just an html.A output. Add html.Div(id="upgrade-cta") to the layout from Step 4.

    app.py
    from usethatapp import manage_url, purchase_url
    
    PRO_PRICE_ID = "<prc-price-id>"   # from get_prices() or your dashboard
    
    
    @callback(
        Output("upgrade-cta", "children"),
        Input("update-button", "n_clicks"),
    )
    def upgrade_cta(_n_clicks):
        token = session.get("uta_access_token")
        if token:
            try:
                ent = get_entitlement(token)
                if ent.entitled and ent.product_public_id == "prod_<your-pro-id>":
                    # Already entitled — offer subscription management instead.
                    return html.A("Manage subscription", href=manage_url())
            except UtaError:
                pass  # fall through to the upgrade CTA
        # Anonymous or not entitled — hosted checkout. No next= means the buyer
        # returns via your registered Login URL, already signed in and entitled.
        return html.A("Upgrade to Pro", href=purchase_url(PRO_PRICE_ID))

    For a live pricing page, render from get_prices() — never hardcode prices. Each Price carries a ready-made buy_url and the product_id that matches the entitlement’s product_public_id. See the Pricing API and Sell from your own website.

    Run it

    app.py
    if __name__ == "__main__":
        app.run(debug=True)   # serves on http://localhost:8050

    Errors

    At the entitlement endpoint, 401 raises UtaTokenError (re-authenticate or refresh) and 403 raises UtaPermissionError (a valid token missing the entitlements scope). Every SDK error inherits from UtaError. See Error Handling and Gotchas.

    AI agents: this site is fully pre-rendered — fetch /llms.txt for an indexed summary of every page, or /llms-full.txt for the whole documentation as one Markdown file.