"""Activation page rendering + license gate inline-renderer. Two public callables: - :func:`render_activation_form` โ€” the activation page body. Used by ``pages/_Activate.py`` for the explicit nav entry and by :func:`require_license_or_render_activation` for the gate-injected inline form. - :func:`require_license_or_render_activation` โ€” call after ``hide_streamlit_chrome`` from every page that needs a valid license. When the license is missing, expired, or invalid it renders the activation form in place of the page body and calls ``st.stop()`` so the rest of the page doesn't execute. - :func:`render_license_status_sidebar` โ€” small chrome widget the sidebar uses to show "Core ยท 327 days left" or "๐Ÿ›‘ Expired". The component is kept dependency-free of the page modules so any page can import these helpers without circular imports through ``components.__init__``. """ from __future__ import annotations from typing import Optional import streamlit as st from src.i18n import t as _t from src.license import ( ExpiredLicenseError, InvalidLicenseError, LicenseError, NotActivatedError, get_manager, ) # --------------------------------------------------------------------------- # Sidebar status widget # --------------------------------------------------------------------------- def render_license_status_sidebar() -> None: """Render the per-page sidebar license badge. Cheap to call โ€” the manager caches the parsed license, so multiple pages mounting the same chrome don't pay multiple disk reads. """ state = get_manager().current_state() target = st.sidebar if not state.activated: target.caption(f"๐Ÿ”’ {_t('license.status_not_activated')}") return if state.error_kind == "invalid": target.caption(f"โš ๏ธ {_t('license.status_invalid')}") return if not state.valid: # Activated but expired. target.caption(f"๐Ÿ›‘ {_t('license.status_expired')}") return tier_label = _t(f"license.tier_{state.tier}") or state.tier.title() if state.tier == "trial": label = _t("license.status_trial", days=state.days_remaining) else: label = _t( "license.status_active", tier=tier_label, days=state.days_remaining, ) target.caption(label) if 0 < state.days_remaining <= 30: target.warning( _t("license.renewal_warning_30", days=state.days_remaining) ) # --------------------------------------------------------------------------- # Activation page body # --------------------------------------------------------------------------- def render_activation_form(*, key_prefix: str = "act") -> None: """Render the full activation page body. *key_prefix* is appended to every Streamlit widget key so the form can be embedded inline by ``require_license_or_render_activation`` without colliding with an instance that's mounted by the explicit ``_Activate.py`` page. """ st.title(_t("activation.title")) st.caption(_t("activation.intro")) mgr = get_manager() state = mgr.current_state() # Surface the current state explicitly so a user with an expired / # invalid license sees what's wrong before they paste anything. if state.activated and state.error_kind == "expired": st.warning( _t("license.renewal_warning_expired", date=state.expires_at[:10]) ) elif state.activated and state.error_kind == "invalid": st.error(f"โš ๏ธ {state.error_message}") st.divider() with st.form(key=f"{key_prefix}_form"): name = st.text_input( _t("activation.name_label"), value=state.name, help=_t("activation.name_help"), key=f"{key_prefix}_name", ) email = st.text_input( _t("activation.email_label"), value=state.email, help=_t("activation.email_help"), key=f"{key_prefix}_email", ) blob = st.text_area( _t("activation.blob_label"), help=_t("activation.blob_help"), key=f"{key_prefix}_blob", height=120, ) is_renewal = state.activated label = ( _t("activation.renew_button") if is_renewal else _t("activation.activate_button") ) submit = st.form_submit_button(label, type="primary") # Process the submission. if submit: if not blob.strip(): st.error(_t("activation.errors_heading") + ": license blob is empty.") return try: if is_renewal: lic = mgr.renew(blob) st.success(_t("activation.renewed", expires=lic.expires_at[:10])) else: lic = mgr.activate_from_blob(blob, name=name, email=email) st.success(_t( "activation.success", name=lic.name, expires=lic.expires_at[:10], )) # Force a clean re-render so the gate sees the new state. st.rerun() except LicenseError as e: st.error(f"{_t('activation.errors_heading')}: {e}") # Deactivate (only useful when already activated). if state.activated: st.divider() with st.expander(_t("activation.deactivate_button")): st.caption(_t("activation.deactivate_help")) if st.button( _t("activation.deactivate_button"), key=f"{key_prefix}_deactivate", type="secondary", ): mgr.deactivate() st.rerun() # --------------------------------------------------------------------------- # Gate # --------------------------------------------------------------------------- def require_license_or_render_activation(*, feature: Optional[str] = None) -> None: """Page-level guard. Call after ``hide_streamlit_chrome``. If a valid license exists (and *feature*, when supplied, is unlocked), this is a no-op. Otherwise it renders the activation form in place of the page body and calls ``st.stop()``. Tool pages that need per-tool gating call :func:`require_feature_or_render_upgrade` *after* this โ€” that keeps the global gate (activate / renew flow) separate from the per-tier feature gate (upgrade flow). """ mgr = get_manager() if mgr.dev_mode: return if mgr.is_valid(): return # Otherwise: render the activation form inline and stop the page. render_activation_form(key_prefix="gate") st.stop() def require_feature_or_render_upgrade(feature: str) -> None: """Per-tool feature gate. Renders an upgrade prompt + stops the page when the active license's tier doesn't unlock *feature*. Call from tool pages after ``hide_streamlit_chrome``. The page body should be wrapped after this call so the upgrade prompt cleanly replaces the body when the user's tier is too low. Distinguished from :func:`require_license_or_render_activation` by what it offers the user: that one drops them into the activation form (paste a paid blob to activate at all); this one surfaces an upgrade prompt with a button to the Activate page (paste a higher-tier blob to upgrade). """ from src.license import UnsupportedFeatureError mgr = get_manager() if mgr.dev_mode: return try: mgr.require_feature(feature) return except UnsupportedFeatureError: pass # render the upgrade prompt below except LicenseError as e: # Not-activated / expired / invalid โ€” fall through to the # global gate (which would already have run; this is belt # and braces for callers that bypass it). st.error(f"โš ๏ธ {e}") st.stop() state = mgr.current_state() tier_label = _t(f"license.tier_{state.tier}") or state.tier.title() feature_names = ", ".join( _t(f"tools.{f}.name") if f.startswith("0") and "_" in f else f for f in state.features ) st.warning(_t("license.feature_locked_title", tier=tier_label)) st.caption(_t("license.feature_locked_body", features=feature_names)) if st.button(_t("license.upgrade_link"), key="upgrade_to_activate"): st.switch_page("pages/_Activate.py") st.stop()