From ce178515f5f4e773132e82134b120ec4f915a2e4 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 27 May 2026 11:03:27 -0700 Subject: [PATCH 1/2] Update usage limit modal for Pro users --- .../billing/components/UsageLimitModal.tsx | 79 ++++++++++++++++--- .../billing/stores/usageLimitStore.test.ts | 24 +++++- .../billing/stores/usageLimitStore.ts | 15 +++- .../features/billing/subscriptions.ts | 5 +- 4 files changed, 106 insertions(+), 17 deletions(-) diff --git a/apps/code/src/renderer/features/billing/components/UsageLimitModal.tsx b/apps/code/src/renderer/features/billing/components/UsageLimitModal.tsx index 5022542a5f..acfe8dce6c 100644 --- a/apps/code/src/renderer/features/billing/components/UsageLimitModal.tsx +++ b/apps/code/src/renderer/features/billing/components/UsageLimitModal.tsx @@ -1,14 +1,23 @@ import { useUsageLimitStore } from "@features/billing/stores/usageLimitStore"; +import { formatResetTime } from "@features/billing/utils"; import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore"; +import { useSeat } from "@hooks/useSeat"; import { WarningCircle } from "@phosphor-icons/react"; import { Button, Dialog, Flex, Text } from "@radix-ui/themes"; +import { trpcClient } from "@renderer/trpc/client"; import { ANALYTICS_EVENTS } from "@shared/types/analytics"; import { track } from "@utils/analytics"; import { useEffect } from "react"; +const SUPPORT_MAILTO = + "mailto:charles@posthog.com?subject=PostHog%20Code%20%E2%80%94%20Pro%20usage%20limit"; + export function UsageLimitModal() { const isOpen = useUsageLimitStore((s) => s.isOpen); + const bucket = useUsageLimitStore((s) => s.bucket); + const resetAt = useUsageLimitStore((s) => s.resetAt); const hide = useUsageLimitStore((s) => s.hide); + const { isPro } = useSeat(); useEffect(() => { if (isOpen) { @@ -26,6 +35,33 @@ export function UsageLimitModal() { useSettingsDialogStore.getState().open("plan-usage"); }; + const handleSupport = () => { + trpcClient.os.openExternal.mutate({ url: SUPPORT_MAILTO }); + }; + + const isDaily = bucket === "burst"; + const isMonthly = bucket === "sustained"; + const resetLabel = resetAt ? formatResetTime(resetAt) : null; + + const title = isDaily + ? "Daily limit reached" + : isMonthly && !isPro + ? "You're out of usage for this month" + : isMonthly + ? "Monthly limit reached" + : "Usage limit reached"; + + const proCapLabel = isDaily + ? "a daily usage cap" + : isMonthly + ? "a monthly usage cap" + : "usage caps"; + const description = isPro + ? `Your Pro plan has ${proCapLabel}.${resetLabel ? ` ${resetLabel}.` : ""}` + : `You've hit your Free ${ + isDaily ? "daily" : "usage" + } limit. Upgrade to Pro for 20x more usage.`; + return ( - - You're out of usage for this month - + {title} - You've hit your Free usage limit. Upgrade to Pro for 20× more - usage. + {description} - - + {isPro ? ( + <> + + + + ) : ( + <> + + + + )} diff --git a/apps/code/src/renderer/features/billing/stores/usageLimitStore.test.ts b/apps/code/src/renderer/features/billing/stores/usageLimitStore.test.ts index 23dc20f0bd..f09d8821f3 100644 --- a/apps/code/src/renderer/features/billing/stores/usageLimitStore.test.ts +++ b/apps/code/src/renderer/features/billing/stores/usageLimitStore.test.ts @@ -3,7 +3,11 @@ import { useUsageLimitStore } from "./usageLimitStore"; describe("usageLimitStore", () => { beforeEach(() => { - useUsageLimitStore.setState({ isOpen: false }); + useUsageLimitStore.setState({ + isOpen: false, + bucket: null, + resetAt: null, + }); }); it("starts closed", () => { @@ -11,9 +15,23 @@ describe("usageLimitStore", () => { expect(state.isOpen).toBe(false); }); - it("show opens the modal", () => { + it("show opens the modal with no context", () => { useUsageLimitStore.getState().show(); - expect(useUsageLimitStore.getState().isOpen).toBe(true); + const state = useUsageLimitStore.getState(); + expect(state.isOpen).toBe(true); + expect(state.bucket).toBeNull(); + expect(state.resetAt).toBeNull(); + }); + + it("show stores bucket and resetAt when provided", () => { + useUsageLimitStore.getState().show({ + bucket: "burst", + resetAt: "2026-01-02T03:04:05Z", + }); + const state = useUsageLimitStore.getState(); + expect(state.isOpen).toBe(true); + expect(state.bucket).toBe("burst"); + expect(state.resetAt).toBe("2026-01-02T03:04:05Z"); }); it("hide closes the modal", () => { diff --git a/apps/code/src/renderer/features/billing/stores/usageLimitStore.ts b/apps/code/src/renderer/features/billing/stores/usageLimitStore.ts index c587db0f36..bbd7e68418 100644 --- a/apps/code/src/renderer/features/billing/stores/usageLimitStore.ts +++ b/apps/code/src/renderer/features/billing/stores/usageLimitStore.ts @@ -1,11 +1,15 @@ import { create } from "zustand"; +export type UsageLimitBucket = "burst" | "sustained"; + interface UsageLimitState { isOpen: boolean; + bucket: UsageLimitBucket | null; + resetAt: string | null; } interface UsageLimitActions { - show: () => void; + show: (args?: { bucket: UsageLimitBucket; resetAt: string }) => void; hide: () => void; } @@ -13,7 +17,14 @@ type UsageLimitStore = UsageLimitState & UsageLimitActions; export const useUsageLimitStore = create()((set) => ({ isOpen: false, + bucket: null, + resetAt: null, - show: () => set({ isOpen: true }), + show: (args) => + set({ + isOpen: true, + bucket: args?.bucket ?? null, + resetAt: args?.resetAt ?? null, + }), hide: () => set({ isOpen: false }), })); diff --git a/apps/code/src/renderer/features/billing/subscriptions.ts b/apps/code/src/renderer/features/billing/subscriptions.ts index 433d3c9ea9..b6a0aeffbc 100644 --- a/apps/code/src/renderer/features/billing/subscriptions.ts +++ b/apps/code/src/renderer/features/billing/subscriptions.ts @@ -20,7 +20,10 @@ export function registerBillingSubscriptions() { if (event.threshold === 100) { if (event.userIsActive) { - useUsageLimitStore.getState().show(); + useUsageLimitStore.getState().show({ + bucket: event.bucket, + resetAt: event.resetAt, + }); return; } toast.error("Usage limit reached", { From aadbb5deae0b0b588058e9ecd689326292e672de Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 27 May 2026 15:53:34 -0700 Subject: [PATCH 2/2] address greptile feedback on usage limit modal --- .../features/billing/components/UsageLimitModal.tsx | 8 +++++--- .../renderer/features/billing/stores/usageLimitStore.ts | 9 ++++++++- apps/code/src/renderer/features/billing/subscriptions.ts | 1 + 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/code/src/renderer/features/billing/components/UsageLimitModal.tsx b/apps/code/src/renderer/features/billing/components/UsageLimitModal.tsx index acfe8dce6c..81e9ab8c33 100644 --- a/apps/code/src/renderer/features/billing/components/UsageLimitModal.tsx +++ b/apps/code/src/renderer/features/billing/components/UsageLimitModal.tsx @@ -16,8 +16,10 @@ export function UsageLimitModal() { const isOpen = useUsageLimitStore((s) => s.isOpen); const bucket = useUsageLimitStore((s) => s.bucket); const resetAt = useUsageLimitStore((s) => s.resetAt); + const eventIsPro = useUsageLimitStore((s) => s.isPro); const hide = useUsageLimitStore((s) => s.hide); - const { isPro } = useSeat(); + const { isPro: seatIsPro } = useSeat(); + const isPro = eventIsPro ?? seatIsPro; useEffect(() => { if (isOpen) { @@ -36,7 +38,7 @@ export function UsageLimitModal() { }; const handleSupport = () => { - trpcClient.os.openExternal.mutate({ url: SUPPORT_MAILTO }); + void trpcClient.os.openExternal.mutate({ url: SUPPORT_MAILTO }); }; const isDaily = bucket === "burst"; @@ -59,7 +61,7 @@ export function UsageLimitModal() { const description = isPro ? `Your Pro plan has ${proCapLabel}.${resetLabel ? ` ${resetLabel}.` : ""}` : `You've hit your Free ${ - isDaily ? "daily" : "usage" + isDaily ? "daily" : isMonthly ? "monthly" : "usage" } limit. Upgrade to Pro for 20x more usage.`; return ( diff --git a/apps/code/src/renderer/features/billing/stores/usageLimitStore.ts b/apps/code/src/renderer/features/billing/stores/usageLimitStore.ts index bbd7e68418..f1c2ca114c 100644 --- a/apps/code/src/renderer/features/billing/stores/usageLimitStore.ts +++ b/apps/code/src/renderer/features/billing/stores/usageLimitStore.ts @@ -6,10 +6,15 @@ interface UsageLimitState { isOpen: boolean; bucket: UsageLimitBucket | null; resetAt: string | null; + isPro: boolean | null; } interface UsageLimitActions { - show: (args?: { bucket: UsageLimitBucket; resetAt: string }) => void; + show: (args?: { + bucket: UsageLimitBucket; + resetAt: string; + isPro?: boolean; + }) => void; hide: () => void; } @@ -19,12 +24,14 @@ export const useUsageLimitStore = create()((set) => ({ isOpen: false, bucket: null, resetAt: null, + isPro: null, show: (args) => set({ isOpen: true, bucket: args?.bucket ?? null, resetAt: args?.resetAt ?? null, + isPro: args?.isPro ?? null, }), hide: () => set({ isOpen: false }), })); diff --git a/apps/code/src/renderer/features/billing/subscriptions.ts b/apps/code/src/renderer/features/billing/subscriptions.ts index b6a0aeffbc..94efa1bb59 100644 --- a/apps/code/src/renderer/features/billing/subscriptions.ts +++ b/apps/code/src/renderer/features/billing/subscriptions.ts @@ -23,6 +23,7 @@ export function registerBillingSubscriptions() { useUsageLimitStore.getState().show({ bucket: event.bucket, resetAt: event.resetAt, + isPro: event.isPro, }); return; }