Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
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";
Comment on lines +12 to +13
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would probably be a good place to use the conversations product so we can set up automations (like message -> slack or something)

not blocking but something to consider moving fwd!


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: seatIsPro } = useSeat();
const isPro = eventIsPro ?? seatIsPro;

useEffect(() => {
if (isOpen) {
Expand All @@ -26,6 +37,33 @@ export function UsageLimitModal() {
useSettingsDialogStore.getState().open("plan-usage");
};

const handleSupport = () => {
void trpcClient.os.openExternal.mutate({ url: SUPPORT_MAILTO });
};
Comment thread
charlesvien marked this conversation as resolved.

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" : isMonthly ? "monthly" : "usage"
} limit. Upgrade to Pro for 20x more usage.`;
Comment thread
charlesvien marked this conversation as resolved.

return (
<Dialog.Root open={isOpen}>
<Dialog.Content
Expand All @@ -36,23 +74,44 @@ export function UsageLimitModal() {
<Flex direction="column" gap="3">
<Flex align="center" gap="2">
<WarningCircle size={20} weight="bold" color="var(--red-9)" />
<Dialog.Title className="mb-0">
You're out of usage for this month
</Dialog.Title>
<Dialog.Title className="mb-0">{title}</Dialog.Title>
</Flex>
<Dialog.Description>
<Text color="gray" className="text-sm">
You've hit your Free usage limit. Upgrade to Pro for 20× more
usage.
{description}
</Text>
</Dialog.Description>
<Flex justify="end" gap="3" mt="2">
<Button type="button" variant="soft" color="gray" onClick={hide}>
Not now
</Button>
<Button type="button" onClick={handleUpgrade}>
See Pro
</Button>
{isPro ? (
<>
<Button
type="button"
variant="soft"
color="gray"
onClick={handleSupport}
mr="auto"
>
Get support
</Button>
<Button type="button" onClick={hide}>
Got it
</Button>
</>
) : (
<>
<Button
type="button"
variant="soft"
color="gray"
onClick={hide}
>
Not now
</Button>
<Button type="button" onClick={handleUpgrade}>
See Pro
</Button>
</>
)}
</Flex>
</Flex>
</Dialog.Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,35 @@ import { useUsageLimitStore } from "./usageLimitStore";

describe("usageLimitStore", () => {
beforeEach(() => {
useUsageLimitStore.setState({ isOpen: false });
useUsageLimitStore.setState({
isOpen: false,
bucket: null,
resetAt: null,
});
});

it("starts closed", () => {
const state = useUsageLimitStore.getState();
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", () => {
Expand Down
22 changes: 20 additions & 2 deletions apps/code/src/renderer/features/billing/stores/usageLimitStore.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
import { create } from "zustand";

export type UsageLimitBucket = "burst" | "sustained";

interface UsageLimitState {
isOpen: boolean;
bucket: UsageLimitBucket | null;
resetAt: string | null;
isPro: boolean | null;
}

interface UsageLimitActions {
show: () => void;
show: (args?: {
bucket: UsageLimitBucket;
resetAt: string;
isPro?: boolean;
}) => void;
hide: () => void;
}

type UsageLimitStore = UsageLimitState & UsageLimitActions;

export const useUsageLimitStore = create<UsageLimitStore>()((set) => ({
isOpen: false,
bucket: null,
resetAt: null,
isPro: null,

show: () => set({ isOpen: true }),
show: (args) =>
set({
isOpen: true,
bucket: args?.bucket ?? null,
resetAt: args?.resetAt ?? null,
isPro: args?.isPro ?? null,
}),
hide: () => set({ isOpen: false }),
}));
6 changes: 5 additions & 1 deletion apps/code/src/renderer/features/billing/subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ export function registerBillingSubscriptions() {

if (event.threshold === 100) {
if (event.userIsActive) {
useUsageLimitStore.getState().show();
useUsageLimitStore.getState().show({
bucket: event.bucket,
resetAt: event.resetAt,
isPro: event.isPro,
});
Comment thread
charlesvien marked this conversation as resolved.
return;
}
toast.error("Usage limit reached", {
Expand Down
Loading