From ef93161840df472e14510c809b9ef9454a2519de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Mon, 3 Feb 2025 20:50:31 +0100 Subject: [PATCH] Add a sticky Built with Reflex badge (#4584) * add the watermark class * remove shortcut exposing badge publicly for now * Rename as "sticky" because "watermark" has a negative connotation * Add config `show_built_with_reflex` This config option is available for authenticated users on various plan tiers * py3.11 compatible f-string * sticky badge inherit from A instead of using on_click/redirect * fix integration test * Move export checking logic to reflex CLI * rx.logo: make it accessible to screen readers Add role="img" aria_label="Reflex" and title="Reflex". * Hide the built with reflex badge for localhost * Revert "fix integration test" This reverts commit a978684d70b59a077b714792603bcefd1939b41a. * experimental: do not show warning for internal imports Only show experimental feature warnings when accessing the names through the rx._x namespace. If reflex internally imports the names via deep imports, then this bypasses the warning to avoid showing it to users that have no control over how the framework uses experimental features. * add help link for show_built_with_reflex option * pre-commit fixes --------- Co-authored-by: Masen Furer --- reflex/app.py | 12 + reflex/components/core/sticky.py | 160 +++++++++ reflex/components/core/sticky.pyi | 449 ++++++++++++++++++++++++++ reflex/components/datadisplay/logo.py | 15 +- reflex/config.py | 3 + reflex/experimental/__init__.py | 28 +- reflex/reflex.py | 29 ++ reflex/utils/prerequisites.py | 39 ++- 8 files changed, 721 insertions(+), 14 deletions(-) create mode 100644 reflex/components/core/sticky.py create mode 100644 reflex/components/core/sticky.pyi diff --git a/reflex/app.py b/reflex/app.py index 060f0346969..d9104ece6c9 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -70,6 +70,7 @@ Default404Page, wait_for_client_redirect, ) +from reflex.components.core.sticky import sticky from reflex.components.core.upload import Upload, get_upload_dir from reflex.components.radix import themes from reflex.config import environment, get_config @@ -875,6 +876,15 @@ def _setup_error_boundary(self): continue self._pages[k] = self._add_error_boundary_to_component(component) + def _setup_sticky_badge(self): + """Add the sticky badge to the app.""" + for k, component in self._pages.items(): + # Would be nice to share single sticky_badge across all pages, but + # it bungles the StatefulComponent compile step. + sticky_badge = sticky() + sticky_badge._add_style_recursive({}) + self._pages[k] = Fragment.create(sticky_badge, component) + def _apply_decorated_pages(self): """Add @rx.page decorated pages to the app. @@ -1005,6 +1015,8 @@ def get_compilation_time() -> str: self._validate_var_dependencies() self._setup_overlay_component() self._setup_error_boundary() + if config.show_built_with_reflex: + self._setup_sticky_badge() progress.advance(task) diff --git a/reflex/components/core/sticky.py b/reflex/components/core/sticky.py new file mode 100644 index 00000000000..162bab3cd7b --- /dev/null +++ b/reflex/components/core/sticky.py @@ -0,0 +1,160 @@ +"""Components for displaying the Reflex sticky logo.""" + +from reflex.components.component import ComponentNamespace +from reflex.components.core.colors import color +from reflex.components.core.cond import color_mode_cond, cond +from reflex.components.core.responsive import tablet_and_desktop +from reflex.components.el.elements.inline import A +from reflex.components.el.elements.media import Path, Rect, Svg +from reflex.components.radix.themes.typography.text import Text +from reflex.experimental.client_state import ClientStateVar +from reflex.style import Style +from reflex.vars.base import Var, VarData + + +class StickyLogo(Svg): + """A simple Reflex logo SVG with only the letter R.""" + + @classmethod + def create(cls): + """Create the simple Reflex logo SVG. + + Returns: + The simple Reflex logo SVG. + """ + return super().create( + Rect.create(width="16", height="16", rx="2", fill="#6E56CF"), + Path.create(d="M10 9V13H12V9H10Z", fill="white"), + Path.create(d="M4 3V13H6V9H10V7H6V5H10V7H12V3H4Z", fill="white"), + width="16", + height="16", + viewBox="0 0 16 16", + xmlns="http://www.w3.org/2000/svg", + ) + + def add_style(self): + """Add the style to the component. + + Returns: + The style of the component. + """ + return Style( + { + "fill": "white", + } + ) + + +class StickyLabel(Text): + """A label that displays the Reflex sticky.""" + + @classmethod + def create(cls): + """Create the sticky label. + + Returns: + The sticky label. + """ + return super().create("Built with Reflex") + + def add_style(self): + """Add the style to the component. + + Returns: + The style of the component. + """ + return Style( + { + "color": color("slate", 1), + "font_weight": "600", + "font_family": "'Instrument Sans', sans-serif", + "font_size": "0.875rem", + "line_height": "1rem", + "letter_spacing": "-0.00656rem", + } + ) + + +class StickyBadge(A): + """A badge that displays the Reflex sticky logo.""" + + @classmethod + def create(cls): + """Create the sticky badge. + + Returns: + The sticky badge. + """ + return super().create( + StickyLogo.create(), + tablet_and_desktop(StickyLabel.create()), + href="https://reflex.dev", + target="_blank", + width="auto", + padding="0.375rem", + align="center", + text_align="center", + ) + + def add_style(self): + """Add the style to the component. + + Returns: + The style of the component. + """ + is_localhost_cs = ClientStateVar.create( + "is_localhost", + default=True, + global_ref=False, + ) + localhost_hostnames = Var.create( + ["localhost", "127.0.0.1", "[::1]"] + ).guess_type() + is_localhost_expr = localhost_hostnames.contains( + Var("window.location.hostname", _var_type=str).guess_type(), + ) + check_is_localhost = Var( + f"useEffect(({is_localhost_cs}) => {is_localhost_cs.set}({is_localhost_expr}), [])", + _var_data=VarData( + imports={"react": "useEffect"}, + ), + ) + is_localhost = is_localhost_cs.value._replace( + merge_var_data=VarData.merge( + check_is_localhost._get_all_var_data(), + VarData(hooks={str(check_is_localhost): None}), + ), + ) + return Style( + { + "position": "fixed", + "bottom": "1rem", + "right": "1rem", + # Do not show the badge on localhost. + "display": cond(is_localhost, "none", "flex"), + "flex-direction": "row", + "gap": "0.375rem", + "align-items": "center", + "width": "auto", + "border-radius": "0.5rem", + "color": color_mode_cond("#E5E7EB", "#27282B"), + "border": color_mode_cond("1px solid #27282B", "1px solid #E5E7EB"), + "background-color": color_mode_cond("#151618", "#FCFCFD"), + "padding": "0.375rem", + "transition": "background-color 0.2s ease-in-out", + "box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)", + "z-index": "9998", + "cursor": "pointer", + }, + ) + + +class StickyNamespace(ComponentNamespace): + """Sticky components namespace.""" + + __call__ = staticmethod(StickyBadge.create) + label = staticmethod(StickyLabel.create) + logo = staticmethod(StickyLogo.create) + + +sticky = StickyNamespace() diff --git a/reflex/components/core/sticky.pyi b/reflex/components/core/sticky.pyi new file mode 100644 index 00000000000..fb27d74ea79 --- /dev/null +++ b/reflex/components/core/sticky.pyi @@ -0,0 +1,449 @@ +"""Stub file for reflex/components/core/sticky.py""" + +# ------------------- DO NOT EDIT ---------------------- +# This file was generated by `reflex/utils/pyi_generator.py`! +# ------------------------------------------------------ +from typing import Any, Dict, Literal, Optional, Union, overload + +from reflex.components.component import ComponentNamespace +from reflex.components.core.breakpoints import Breakpoints +from reflex.components.el.elements.inline import A +from reflex.components.el.elements.media import Svg +from reflex.components.radix.themes.typography.text import Text +from reflex.event import BASE_STATE, EventType +from reflex.style import Style +from reflex.vars.base import Var + +class StickyLogo(Svg): + @overload + @classmethod + def create( # type: ignore + cls, + *children, + width: Optional[Union[Var[Union[int, str]], int, str]] = None, + height: Optional[Union[Var[Union[int, str]], int, str]] = None, + xmlns: Optional[Union[Var[str], str]] = None, + access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + auto_capitalize: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + content_editable: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + context_menu: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + enter_key_hint: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_blur: Optional[EventType[[], BASE_STATE]] = None, + on_click: Optional[EventType[[], BASE_STATE]] = None, + on_context_menu: Optional[EventType[[], BASE_STATE]] = None, + on_double_click: Optional[EventType[[], BASE_STATE]] = None, + on_focus: Optional[EventType[[], BASE_STATE]] = None, + on_mount: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_down: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_move: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_out: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_over: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_up: Optional[EventType[[], BASE_STATE]] = None, + on_scroll: Optional[EventType[[], BASE_STATE]] = None, + on_unmount: Optional[EventType[[], BASE_STATE]] = None, + **props, + ) -> "StickyLogo": + """Create the simple Reflex logo SVG. + + Returns: + The simple Reflex logo SVG. + """ + ... + + def add_style(self): ... + +class StickyLabel(Text): + @overload + @classmethod + def create( # type: ignore + cls, + *children, + as_child: Optional[Union[Var[bool], bool]] = None, + as_: Optional[ + Union[ + Literal[ + "abbr", + "b", + "cite", + "del", + "div", + "em", + "i", + "ins", + "kbd", + "label", + "mark", + "p", + "s", + "samp", + "span", + "sub", + "sup", + "u", + ], + Var[ + Literal[ + "abbr", + "b", + "cite", + "del", + "div", + "em", + "i", + "ins", + "kbd", + "label", + "mark", + "p", + "s", + "samp", + "span", + "sub", + "sup", + "u", + ] + ], + ] + ] = None, + size: Optional[ + Union[ + Breakpoints[str, Literal["1", "2", "3", "4", "5", "6", "7", "8", "9"]], + Literal["1", "2", "3", "4", "5", "6", "7", "8", "9"], + Var[ + Union[ + Breakpoints[ + str, Literal["1", "2", "3", "4", "5", "6", "7", "8", "9"] + ], + Literal["1", "2", "3", "4", "5", "6", "7", "8", "9"], + ] + ], + ] + ] = None, + weight: Optional[ + Union[ + Breakpoints[str, Literal["bold", "light", "medium", "regular"]], + Literal["bold", "light", "medium", "regular"], + Var[ + Union[ + Breakpoints[str, Literal["bold", "light", "medium", "regular"]], + Literal["bold", "light", "medium", "regular"], + ] + ], + ] + ] = None, + align: Optional[ + Union[ + Breakpoints[str, Literal["center", "left", "right"]], + Literal["center", "left", "right"], + Var[ + Union[ + Breakpoints[str, Literal["center", "left", "right"]], + Literal["center", "left", "right"], + ] + ], + ] + ] = None, + trim: Optional[ + Union[ + Breakpoints[str, Literal["both", "end", "normal", "start"]], + Literal["both", "end", "normal", "start"], + Var[ + Union[ + Breakpoints[str, Literal["both", "end", "normal", "start"]], + Literal["both", "end", "normal", "start"], + ] + ], + ] + ] = None, + color_scheme: Optional[ + Union[ + Literal[ + "amber", + "blue", + "bronze", + "brown", + "crimson", + "cyan", + "gold", + "grass", + "gray", + "green", + "indigo", + "iris", + "jade", + "lime", + "mint", + "orange", + "pink", + "plum", + "purple", + "red", + "ruby", + "sky", + "teal", + "tomato", + "violet", + "yellow", + ], + Var[ + Literal[ + "amber", + "blue", + "bronze", + "brown", + "crimson", + "cyan", + "gold", + "grass", + "gray", + "green", + "indigo", + "iris", + "jade", + "lime", + "mint", + "orange", + "pink", + "plum", + "purple", + "red", + "ruby", + "sky", + "teal", + "tomato", + "violet", + "yellow", + ] + ], + ] + ] = None, + high_contrast: Optional[Union[Var[bool], bool]] = None, + access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + auto_capitalize: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + content_editable: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + context_menu: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + enter_key_hint: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_blur: Optional[EventType[[], BASE_STATE]] = None, + on_click: Optional[EventType[[], BASE_STATE]] = None, + on_context_menu: Optional[EventType[[], BASE_STATE]] = None, + on_double_click: Optional[EventType[[], BASE_STATE]] = None, + on_focus: Optional[EventType[[], BASE_STATE]] = None, + on_mount: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_down: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_move: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_out: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_over: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_up: Optional[EventType[[], BASE_STATE]] = None, + on_scroll: Optional[EventType[[], BASE_STATE]] = None, + on_unmount: Optional[EventType[[], BASE_STATE]] = None, + **props, + ) -> "StickyLabel": + """Create the sticky label. + + Returns: + The sticky label. + """ + ... + + def add_style(self): ... + +class StickyBadge(A): + @overload + @classmethod + def create( # type: ignore + cls, + *children, + download: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + href: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + href_lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + media: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + ping: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + referrer_policy: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + rel: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + shape: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + target: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + auto_capitalize: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + content_editable: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + context_menu: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + enter_key_hint: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_blur: Optional[EventType[[], BASE_STATE]] = None, + on_click: Optional[EventType[[], BASE_STATE]] = None, + on_context_menu: Optional[EventType[[], BASE_STATE]] = None, + on_double_click: Optional[EventType[[], BASE_STATE]] = None, + on_focus: Optional[EventType[[], BASE_STATE]] = None, + on_mount: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_down: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_move: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_out: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_over: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_up: Optional[EventType[[], BASE_STATE]] = None, + on_scroll: Optional[EventType[[], BASE_STATE]] = None, + on_unmount: Optional[EventType[[], BASE_STATE]] = None, + **props, + ) -> "StickyBadge": + """Create the sticky badge. + + Returns: + The sticky badge. + """ + ... + + def add_style(self): ... + +class StickyNamespace(ComponentNamespace): + label = staticmethod(StickyLabel.create) + logo = staticmethod(StickyLogo.create) + + @staticmethod + def __call__( + *children, + download: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + href: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + href_lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + media: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + ping: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + referrer_policy: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + rel: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + shape: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + target: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + auto_capitalize: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + content_editable: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + context_menu: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + enter_key_hint: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_blur: Optional[EventType[[], BASE_STATE]] = None, + on_click: Optional[EventType[[], BASE_STATE]] = None, + on_context_menu: Optional[EventType[[], BASE_STATE]] = None, + on_double_click: Optional[EventType[[], BASE_STATE]] = None, + on_focus: Optional[EventType[[], BASE_STATE]] = None, + on_mount: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_down: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_move: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_out: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_over: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_up: Optional[EventType[[], BASE_STATE]] = None, + on_scroll: Optional[EventType[[], BASE_STATE]] = None, + on_unmount: Optional[EventType[[], BASE_STATE]] = None, + **props, + ) -> "StickyBadge": + """Create the sticky badge. + + Returns: + The sticky badge. + """ + ... + +sticky = StickyNamespace() diff --git a/reflex/components/datadisplay/logo.py b/reflex/components/datadisplay/logo.py index 1c4c0200150..dab6d246840 100644 --- a/reflex/components/datadisplay/logo.py +++ b/reflex/components/datadisplay/logo.py @@ -5,11 +5,15 @@ import reflex as rx -def svg_logo(color: Union[str, rx.Var[str]] = rx.color_mode_cond("#110F1F", "white")): +def svg_logo( + color: Union[str, rx.Var[str]] = rx.color_mode_cond("#110F1F", "white"), + **props, +): """A Reflex logo SVG. Args: color: The color of the logo. + props: Extra props to pass to the svg component. Returns: The Reflex logo SVG. @@ -29,11 +33,14 @@ def logo_path(d: str): return rx.el.svg( *[logo_path(d) for d in paths], - width="56", - height="12", - viewBox="0 0 56 12", + rx.el.title("Reflex"), + aria_label="Reflex", + role="img", + width=props.pop("width", "56"), + height=props.pop("height", "12"), fill=color, xmlns="http://www.w3.org/2000/svg", + **props, ) diff --git a/reflex/config.py b/reflex/config.py index 6609067f964..050676227be 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -703,6 +703,9 @@ class Config: # pyright: ignore [reportIncompatibleVariableOverride] # Path to file containing key-values pairs to override in the environment; Dotenv format. env_file: Optional[str] = None + # Whether to display the sticky "Built with Reflex" badge on all pages. + show_built_with_reflex: bool = True + # Whether the app is running in the reflex cloud environment. is_reflex_cloud: bool = False diff --git a/reflex/experimental/__init__.py b/reflex/experimental/__init__.py index 1a198f35a12..7971c33ae17 100644 --- a/reflex/experimental/__init__.py +++ b/reflex/experimental/__init__.py @@ -13,16 +13,25 @@ from .layout import layout as layout from .misc import run_in_thread as run_in_thread -warn( - "`rx._x` contains experimental features and might be removed at any time in the future .", -) - -_EMITTED_PROMOTION_WARNINGS = set() - class ExperimentalNamespace(SimpleNamespace): """Namespace for experimental features.""" + def __getattribute__(self, item: str): + """Get attribute from the namespace. + + Args: + item: attribute name. + + Returns: + The attribute. + """ + warn( + "`rx._x` contains experimental features and might be removed at any time in the future.", + dedupe=True, + ) + return super().__getattribute__(item) + @property def toast(self): """Temporary property returning the toast namespace. @@ -55,9 +64,10 @@ def register_component_warning(component_name: str): Args: component_name: name of the component. """ - if component_name not in _EMITTED_PROMOTION_WARNINGS: - _EMITTED_PROMOTION_WARNINGS.add(component_name) - warn(f"`rx._x.{component_name}` was promoted to `rx.{component_name}`.") + warn( + f"`rx._x.{component_name}` was promoted to `rx.{component_name}`.", + dedupe=True, + ) _x = ExperimentalNamespace( diff --git a/reflex/reflex.py b/reflex/reflex.py index d1e56566595..70aa16a0510 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -26,6 +26,8 @@ # Fallback for older typer versions. cli = typer.Typer(add_completion=False) +SHOW_BUILT_WITH_REFLEX_INFO = "https://reflex.dev/docs/hosting/reflex-branding/" + # Get the config. config = get_config() @@ -186,6 +188,15 @@ def _run( prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME) if frontend: + if not config.show_built_with_reflex: + # The sticky badge may be disabled at runtime for team/enterprise tiers. + prerequisites.check_config_option_in_tier( + option_name="show_built_with_reflex", + allowed_tiers=["team", "enterprise"], + fallback_value=True, + help_link=SHOW_BUILT_WITH_REFLEX_INFO, + ) + # Get the app module. prerequisites.get_compiled_app() @@ -324,6 +335,15 @@ def export( if prerequisites.needs_reinit(frontend=True): _init(name=config.app_name, loglevel=loglevel) + if frontend and not config.show_built_with_reflex: + # The sticky badge may be disabled on export for team/enterprise tiers. + prerequisites.check_config_option_in_tier( + option_name="show_built_with_reflex", + allowed_tiers=["team", "enterprise"], + fallback_value=False, + help_link=SHOW_BUILT_WITH_REFLEX_INFO, + ) + export_utils.export( zipping=zipping, frontend=frontend, @@ -518,6 +538,15 @@ def deploy( check_version() + if not config.show_built_with_reflex: + # The sticky badge may be disabled on deploy for pro/team/enterprise tiers. + prerequisites.check_config_option_in_tier( + option_name="show_built_with_reflex", + allowed_tiers=["pro", "team", "enterprise"], + fallback_value=True, + help_link=SHOW_BUILT_WITH_REFLEX_INFO, + ) + # Set the log level. console.set_log_level(loglevel) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 4741400f896..629198185d3 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -23,7 +23,7 @@ from datetime import datetime from pathlib import Path from types import ModuleType -from typing import Callable, List, NamedTuple, Optional +from typing import Any, Callable, List, NamedTuple, Optional import httpx import typer @@ -1978,3 +1978,40 @@ def is_generation_hash(template: str) -> bool: True if the template is composed of 32 or more hex characters. """ return re.match(r"^[0-9a-f]{32,}$", template) is not None + + +def check_config_option_in_tier( + option_name: str, + allowed_tiers: list[str], + fallback_value: Any, + help_link: str | None = None, +): + """Check if a config option is allowed for the authenticated user's current tier. + + Args: + option_name: The name of the option to check. + allowed_tiers: The tiers that are allowed to use the option. + fallback_value: The fallback value if the option is not allowed. + help_link: The help link to show to a user that is authenticated. + """ + from reflex_cli.v2.utils import hosting + + config = get_config() + authenticated_token = hosting.authenticated_token() + if not authenticated_token[0]: + the_remedy = ( + "You are currently logged out. Run `reflex login` to access this option." + ) + current_tier = "anonymous" + else: + current_tier = authenticated_token[1].get("tier", "").lower() + the_remedy = ( + f"Your current subscription tier is `{current_tier}`. " + f"Please upgrade to {allowed_tiers} to access this option. " + ) + if help_link: + the_remedy += f"See {help_link} for more information." + if current_tier not in allowed_tiers: + console.warn(f"Config option `{option_name}` is restricted. {the_remedy}") + setattr(config, option_name, fallback_value) + config._set_persistent(**{option_name: fallback_value})