diff --git a/pcweb/pages/pricing/calculator.py b/pcweb/pages/pricing/calculator.py index 7f92c217ea..ee87a9ddf3 100644 --- a/pcweb/pages/pricing/calculator.py +++ b/pcweb/pages/pricing/calculator.py @@ -30,14 +30,14 @@ def seat_rate(self) -> int: if self.selected_plan == Tiers.PRO.value: return 20 elif self.selected_plan == Tiers.TEAM.value: - return 29 + return 50 @rx.var(cache=True) def max_seats(self) -> int: if self.selected_plan == Tiers.PRO.value: return 5 elif self.selected_plan == Tiers.TEAM.value: - return 15 + return 25 @rx.var(cache=True) def max_cpu(self) -> int: @@ -57,12 +57,19 @@ def max_ram(self) -> int: def change_plan(self, plan: str) -> None: self.selected_plan = plan if plan == Tiers.PRO.value: - if self.estimated_cpu_number > 5: - self.estimated_cpu_number = 5 - if self.estimated_ram_gb > 10: - self.estimated_ram_gb = 10 - if self.estimated_seats > 5: - self.estimated_seats = 5 + self.included_cpu = 1 + self.included_ram = .5 + self.included_seats = 1 + # Enforce Pro tier limits + self.estimated_cpu_number = min(self.estimated_cpu_number, 5) + self.estimated_ram_gb = min(self.estimated_ram_gb, 10) + self.estimated_seats = min(self.estimated_seats, 5) + else: + self.included_cpu = 2 + self.included_ram = 3 + self.included_seats = 5 + # Enforce Team tier minimum seats + self.estimated_seats = max(5, self.estimated_seats) def calculator(text: str, component: rx.Component, total: str) -> rx.Component: @@ -144,6 +151,21 @@ def stepper( def pricing_widget() -> rx.Component: return rx.box( rx.box( + # Tier + calculator( + "Tier", + rx.box( + rx.segmented_control.root( + rx.segmented_control.item("Pro", value="Pro"), + #rx.segmented_control.item("Team (coming soon)", value="Team"), + on_change=BillingState.change_plan, + default_value="Pro", + width="100%", + ), + class_name="flex flex-row pt-2 !w-[8.5rem] !h-[2.25rem] mb-2", + ), + "", + ), # Team seats calculator( "Members", @@ -201,10 +223,12 @@ def pricing_widget() -> rx.Component: rx.center( rx.flex( rx.badge( - f"Total: ${calculate_total()}/month", + f"Total: ${calculate_total()}- $20 free credits = ", + rx.text.strong(f"${calculate_total()-20}/mo"), + + size='3', ), - size='3', - class_name="mt-6", + class_name="mt-6", ) ), class_name="flex-1 flex flex-col relative h-full w-full max-w-[25rem] pb-2.5 z-[2]", @@ -212,28 +236,40 @@ def pricing_widget() -> rx.Component: def calculate_total(): - return round( - BillingState.estimated_seats * BillingState.seat_rate - + BillingState.estimated_ram_gb * (BillingState.mem_rate * MONTH_MINUTES) - + BillingState.estimated_cpu_number * (BillingState.cpu_rate * MONTH_MINUTES) - - 25 + # Base price using rx.cond + base_price = rx.cond( + BillingState.selected_plan == Tiers.PRO.value, + 20, + 250 + ) + + # Calculate additional seats cost + additional_seats = rx.cond( + BillingState.estimated_seats > 1, + BillingState.estimated_seats - 1, + 0 ) + seat_cost = additional_seats * BillingState.seat_rate + + compute_cost = ( + (BillingState.estimated_ram_gb) * (BillingState.mem_rate * MONTH_MINUTES) + + (BillingState.estimated_cpu_number) * (BillingState.cpu_rate * MONTH_MINUTES) + ) + + total = base_price + seat_cost + compute_cost + return round(total) def header() -> rx.Component: return rx.box( rx.el.h3( - "Calculate costs.", + "Cost Estimate", class_name="text-slate-12 text-3xl font-semibold text-center", id="calculator-header", ), rx.el.p( - "Simply usage based pricing.", - class_name="text-slate-9 text-3xl font-semibold text-center", - ), - rx.el.p( - "We subtract the hobby tier free CPU and RAM from your usage.", - class_name="text-slate-9 text-md font-medium text-center mt-2", + "Get a price estimate for your organization.", + class_name="text-slate-9 text-2xl font-semibold text-center", ), class_name="flex items-center mb-5 justify-between text-slate-11 flex-col pt-[5rem] mx-auto w-full", ) diff --git a/pcweb/pages/pricing/plan_cards.py b/pcweb/pages/pricing/plan_cards.py index c2b7011d67..65919995ea 100644 --- a/pcweb/pages/pricing/plan_cards.py +++ b/pcweb/pages/pricing/plan_cards.py @@ -150,6 +150,10 @@ def card( rx.el.li( rx.icon(feature[0], class_name="!text-slate-9", size=16), feature[1], + rx.tooltip( + rx.icon("info", class_name="!text-slate-9", size=12), + content=feature[2], + ) if len(feature) == 3 else "", class_name="text-sm font-medium text-slate-11 flex items-center gap-3", ) for feature in features @@ -222,12 +226,12 @@ def plan_cards() -> rx.Component: return rx.box( card( "Hobby", - "Everything you need to get started with Reflex.", + "Everything you need to get started.", [ - ("code", "Open Source Framework"), ("heart-handshake", "Community support"), - ("user", "1 team member"), - ("app-window", "1 Deployed app"), + ("app-window", "1 app included", "Free apps shutdown after 1 hour of inactivity, and are restarted on demand (usually within a few seconds)."), + ("clock", "1 day log retention"), + ("code", "Open Source Framework"), ("file-code", "Starter Templates"), ], "Start building for free", @@ -236,48 +240,46 @@ def plan_cards() -> rx.Component: popular_card( "Pro", "For professional projects and startups.", - [ - ("server", "Larger machine sizes"), - ("users", "Up to 5 team members"), - ("app-window", "Up to 5 Deployed apps"), + [ + ("heart-handshake", "Community support"), + ("users", "1 seat incl. (additional seats at $20/mo)"), + ("credit-card", "$20 / month free compute credits"), + ("app-window", "Up to 5 apps"), + ("server", "Customizable machine sizes"), ("clock", "30 days log retention"), ("globe", "Multi-region"), ("brush", "Custom domains"), - ("wand", "AI Tools for Building and Debugging"), - ("circle-plus", "Everything in Hobby"), ], "Start with Pro plan", - price="$20/mo + usage", + price="$20/mo + compute", ), card( "Team", "For teams looking to scale their applications.", [ - ("mail", "Email support"), - ("users", "Up to 25 team members"), - ("app-window", "Unlimited Apps"), + ("mail", "Email/Slack support"), + ("users", "5 seat incl. (additional seats available)"), + ("credit-card", "Monthly free compute credits"), + ("app-window", "Unlimited apps"), + ("git-branch", "Create multiple projects"), ("signal", "Full Website Analytics"), ("lock-keyhole", "One Click Auth"), - ("git-branch", "Dev, Stage & Prod Envs"), - ("database", "DB Editor UI and Migration Tool"), - ("test-tube", "Built-in Testing Framework"), ("circle-plus", "Everything in Pro"), ], "Contact sales", ), card( "Enterprise", - "Get our priority support and a plan tailored to your needs.", + "Get a plan tailored to your business needs.", [ ("headset", "Priority Engineering Support"), - ("user-round-plus", "White Glove Onboarding"), - ("users", "Unlimited team members"), + ("users", "Customized seat amount"), + ("user-round-plus", "Personalized integration help"), ("hard-drive", "On Premise Deployment"), - ("signal", "Full Analytics Dashboard"), ("clock", "Unlimited log retention"), ("activity", "Error Monitoring and Observability"), ("git-pull-request", "Influence Reflex Roadmap"), - ("shield-check", "Custom SSO"), + ("shield-check", "Audit logs, SSO, SOC2 Reports"), ("circle-plus", "Everything in Team"), ], "Contact sales", diff --git a/pcweb/pages/pricing/table.py b/pcweb/pages/pricing/table.py index e7aea5184d..3499dd1b2e 100644 --- a/pcweb/pages/pricing/table.py +++ b/pcweb/pages/pricing/table.py @@ -6,6 +6,7 @@ STYLES = { "cell": "text-slate-12 font-medium text-sm whitespace-nowrap", "header_cell": "text-slate-12 font-semibold text-lg", + "header_cell_sub": "text-slate-11 font-semibold text-md", "feature_cell": "text-slate-9 font-medium text-sm whitespace-nowrap", "button_base": "!text-sm !font-semibold w-full text-nowrap", } @@ -32,27 +33,16 @@ } """ -# Data configuration -USERS_SECTION = [ - ("Per Seat Price", "Free", "$20/mo/user", "Contact Sales", "Contact Sales"), - ("User Limit", "1", "5", "25", "Unlimited"), -] - FRAMEWORK_SECTION = [ ("Open Source Framework", True, True, True, True), - ("Starter Templates", True, True, True, True), - ("Enterprise Templates", False, False, True, True), + ("Templates", True, True, True, True), ("One Click Auth", False, False, True, True), ("Embed Reflex Apps", False, False, True, True), ("Built-in Testing", False, False, True, True), ] -THEME_SECTION = [("Theming", "Builtin Themes", "Builtin Themes", "Custom Themes", "Custom Themes")] - REFLEX_AI_SECTION = [ - ("Flexgen Website Builder", "5/day", "20/day", "100/day", "Custom"), - ("Full-Stack AI Agent", "5/day", "50/day", "250/day", "Custom"), - ("AI Assistant / Debugger", "5/day", "50/day", "250/day", "Custom"), + ("Number of Generations", "5/month", "100/month/seat", "250/month/seat", "Custom"), ] DATABASE_SECTION = [ @@ -64,13 +54,13 @@ HOSTING_TEXT_SECTION = [ ("Compute Limits", "1 CPU, .5GB", "5 CPU, 10GB", "Custom", "Custom"), ("Regions", "Single", "Multiple", "Multiple", "Multiple"), - ("Custom Domains", "None", "1", "5", "Unlimited"), ("Build logs", "7 day", "30 days", "90 days", "Custom"), ("Runtime logs", "1 day", "7 days", "30 days", "Custom"), ] HOSTING_BOOLEAN_SECTION = [ ("CLI Deployments", True, True, True, True), + ("Custom Domains", False, True, True, True), ("Automatic CI / CD Deploy (Github)", False, False, True, True), ("Secrets", True, True, True, True), ("Secret Manager", False, False, True, True), @@ -90,11 +80,11 @@ ("Rich Permissions Control", False, False, True, True), ("Connect to Analytics Vendors", False, False, True, True), ("Audit Logs", False, False, False, True), - ("Custom SSO", False, False, False, True), + ("SSO", False, False, False, True), ] SUPPORT_TEXT_SECTION = [ - ("Support", "Community", "Community", "Email Support", "Dedicated Support") + ("Support", "Community", "Community", "Email/Slack", "Dedicated Support") ] SUPPORT_BOOLEAN_SECTION = [ @@ -154,15 +144,18 @@ def create_table_row(cells: list) -> rx.Component: row_cells = [create_table_cell(cell) for cell in cells] return rx.table.row( *row_cells, - class_name="w-full [&>*:not(:first-child)]:text-center bg-slate-1 z-[2] !h-[56px]", + class_name="w-full [&>*:not(:first-child)]:text-center bg-slate-1 z-[2] !h-[50px]", ) -def create_table_row_header(cells: list, coming_soon: bool = False) -> rx.Component: +def create_table_row_header(name: list, coming_soon: bool = False) -> rx.Component: return rx.table.row( *[ - rx.table.column_header_cell(cell, rx.badge("coming soon", margin_left="0.5rem"), class_name=STYLES["header_cell"]) if cell and coming_soon else rx.table.column_header_cell(cell, class_name=STYLES["header_cell"]) - for cell in cells + rx.table.column_header_cell(name, rx.badge("coming soon", margin_left="0.5rem"), class_name=STYLES["header_cell"]) if coming_soon else rx.table.column_header_cell(name, class_name=STYLES["header_cell"]), + rx.table.column_header_cell("Hobby", class_name=STYLES["header_cell_sub"]), + rx.table.column_header_cell("Pro", class_name=STYLES["header_cell_sub"]), + rx.table.column_header_cell("Team", class_name=STYLES["header_cell_sub"]), + rx.table.column_header_cell("Enterprise", class_name=STYLES["header_cell_sub"]) ], class_name="w-full [&>*:not(:first-child)]:text-center bg-slate-2 border border-slate-3 rounded-2xl z-[6] !h-[3.625rem] relative", padding_x="5rem !important", @@ -223,17 +216,10 @@ def table_body_hosting() -> rx.Component: return rx.table.root( rx.el.style(TABLE_STYLE), rx.table.header( - create_table_row_header(["Price", "Hobby", "Pro", "Team", "Enterprise"]), + create_table_row_header("Hosting"), glow(), class_name="relative", ), - create_table_body( - *[create_table_row(row) for row in USERS_SECTION], - ), - rx.table.header( - create_table_row_header(["Hosting", "", "", ""]), - class_name="relative", - ), create_table_body( *[create_table_row(row) for row in HOSTING_TEXT_SECTION], *[ @@ -242,7 +228,7 @@ def table_body_hosting() -> rx.Component: ], ), rx.table.header( - create_table_row_header(["Security", "", "", "", ""]), + create_table_row_header("Security"), class_name="relative", ), create_table_body( @@ -252,7 +238,7 @@ def table_body_hosting() -> rx.Component: ], ), rx.table.header( - create_table_row_header(["Support", "", "", "", ""]), + create_table_row_header("Support"), class_name="relative", ), create_table_body( @@ -270,7 +256,7 @@ def table_body_oss() -> rx.Component: return rx.table.root( rx.el.style(TABLE_STYLE), rx.table.header( - create_table_row_header(["Framework","Hobby", "Pro", "Team", "Enterprise"]), + create_table_row_header("Framework"), class_name="relative", ), create_table_body( @@ -278,10 +264,9 @@ def table_body_oss() -> rx.Component: create_checkmark_row(feature, checks) for feature, *checks in FRAMEWORK_SECTION ], - *[create_table_row(row) for row in THEME_SECTION], ), rx.table.header( - create_table_row_header(["Database", "", "", ""]), + create_table_row_header("Database"), class_name="relative", ), create_table_body( @@ -291,7 +276,7 @@ def table_body_oss() -> rx.Component: ], ), rx.table.header( - create_table_row_header(["AI", "", "", ""], coming_soon=True), + create_table_row_header("AI", coming_soon=True), class_name="relative", ), create_table_body(