Skip to content

Commit 05ecf2a

Browse files
authored
fix(billing): apply local-testing polish to the billing surfaces
formatResetTime rolls 60 minutes into the next hour; isSameUsage compares code_usage_subscribed and ai_credits.exhausted so a mid-session subscribe flip emits UsageUpdated; the plans page keeps only the monthly free meter (the free valve is $20 on both windows, so the monthly cap binds); the announcement modal leads with the headline and tightens the bullets; the cloud preflight reset hint prefers the sustained window. Generated-By: PostHog Code Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
1 parent c8ff7f0 commit 05ecf2a

7 files changed

Lines changed: 56 additions & 31 deletions

File tree

packages/core/src/billing/usageDisplay.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ describe("formatResetTime", () => {
9595
resetAt: isoAt(4 * 3600 * 1000),
9696
expected: "Resets in 4h",
9797
},
98+
{
99+
name: "rolls the hour instead of showing 60 minutes",
100+
resetAt: isoAt((23 * 3600 + 59 * 60 + 40) * 1000),
101+
expected: "Resets in 24h",
102+
},
98103
{
99104
name: "returns localized date when over 24h away",
100105
resetAt: isoAt(30 * 86400 * 1000),

packages/core/src/billing/usageDisplay.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ export function formatResetTime(
2626

2727
const totalHours = ms / 3_600_000;
2828
if (totalHours < 24) {
29-
const hours = Math.floor(totalHours);
30-
const minutes = Math.round((totalHours - hours) * 60);
29+
let hours = Math.floor(totalHours);
30+
let minutes = Math.round((totalHours - hours) * 60);
31+
if (minutes === 60) {
32+
hours += 1;
33+
minutes = 0;
34+
}
3135
return minutes === 0
3236
? `Resets in ${hours}h`
3337
: `Resets in ${hours}h ${minutes}m`;

packages/core/src/usage/usage-monitor.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,29 @@ describe("UsageMonitorService", () => {
246246
expect(updates[1].burst.used_percent).toBe(35);
247247
});
248248

249+
// The titlebar meter keys off this bit; a subscribe flip must not wait for
250+
// some other field to change.
251+
it("emits UsageUpdated when only the subscription bit flips", async () => {
252+
const updates: UsageOutput[] = [];
253+
const gateway = {
254+
fetchUsage: vi
255+
.fn()
256+
.mockResolvedValueOnce({
257+
...makeUsage(),
258+
code_usage_subscribed: false,
259+
})
260+
.mockResolvedValueOnce({ ...makeUsage(), code_usage_subscribed: true }),
261+
} as unknown as GatewaySlice;
262+
service = makeService(gateway, makeActivityMonitor());
263+
service.on(UsageMonitorEvent.UsageUpdated, (u) => updates.push(u));
264+
265+
await service.fetchOnce();
266+
await service.fetchOnce();
267+
268+
expect(updates).toHaveLength(2);
269+
expect(updates[1].code_usage_subscribed).toBe(true);
270+
});
271+
249272
it("does not emit UsageUpdated when the gateway throws", async () => {
250273
const updates: UsageOutput[] = [];
251274
const gateway = {

packages/core/src/usage/usage-monitor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ function isSameUsage(a: UsageOutput | null, b: UsageOutput): boolean {
245245
return (
246246
a.is_rate_limited === b.is_rate_limited &&
247247
a.billing_period_end === b.billing_period_end &&
248+
a.code_usage_subscribed === b.code_usage_subscribed &&
249+
a.ai_credits?.exhausted === b.ai_credits?.exhausted &&
248250
isSameBucket(a.burst, b.burst) &&
249251
isSameBucket(a.sustained, b.sustained)
250252
);

packages/ui/src/features/billing/UsageBillingAnnouncementModal.tsx

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,25 +65,22 @@ export function UsageBillingAnnouncementModal() {
6565
</Dialog.Title>
6666
</Flex>
6767
<Dialog.Description>
68-
<Text color="gray" className="text-sm">
69-
Seat-based plans are gone. PostHog Code is now usage-based — your
70-
organization pays for AI usage at cost, and you only pay for what
71-
you use.
68+
<Text className="text-sm">
69+
Seat-based plans are gone — PostHog Code is now usage-based. You
70+
only pay for what you use.
7271
</Text>
7372
</Dialog.Description>
7473
<Flex direction="column" gap="2" className="text-sm">
75-
<Text>
76-
Your organization's first <Text weight="medium">$20</Text> of
77-
usage each month is included.
74+
<Text color="gray">
75+
The first <Text weight="medium">$20</Text> of usage each month
76+
is included.
7877
</Text>
79-
<Text>
80-
• Premium models (Claude, GPT) need a payment method on your
81-
organization; an open model stays available on the free tier.
78+
<Text color="gray">
79+
• Premium models need a payment method; an open model stays free.
8280
</Text>
83-
<Text>
84-
• Every organization starts with a{" "}
85-
<Text weight="medium">$50/month</Text> spend limit you can raise,
86-
lower, or remove in PostHog billing settings.
81+
<Text color="gray">
82+
• A default <Text weight="medium">$50/month</Text> spend limit
83+
applies — adjust it any time in billing settings.
8784
</Text>
8885
</Flex>
8986
<Flex justify="end" gap="3" mt="2">

packages/ui/src/features/billing/preflightCloudUsage.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ import { type UsageLimitShowArgs, useUsageLimitStore } from "./usageLimitStore";
1313
const log = logger.scope("preflight-cloud-usage");
1414

1515
function usageLimitArgs(usage: UsageOutput): UsageLimitShowArgs {
16-
// Reset hint from whichever bucket is exceeded (daily takes priority);
17-
// the monthly bucket covers an org-bucket block, whose period it matches.
18-
const bucket = usage.burst.exceeded ? "burst" : "sustained";
16+
// Burst-alone is the only state the earlier daily reset unblocks; both
17+
// exceeded or an org-bucket block wait for the sustained window.
18+
const bucket =
19+
usage.burst.exceeded && !usage.sustained.exceeded ? "burst" : "sustained";
1920
return { resetAt: usage[bucket].reset_at, cause: "org_limit" };
2021
}
2122

packages/ui/src/features/settings/sections/PlanUsageSettings.tsx

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -168,18 +168,11 @@ export function PlanUsageSettings() {
168168
<Spinner size="2" />
169169
</Flex>
170170
) : freeTier && usage ? (
171-
<Flex direction="column" gap="3">
172-
<UsageMeter
173-
label="Monthly free usage"
174-
bucket={usage.sustained}
175-
color={usage.sustained.exceeded ? "red" : undefined}
176-
/>
177-
<UsageMeter
178-
label="Daily free usage"
179-
bucket={usage.burst}
180-
color={usage.burst.exceeded ? "red" : undefined}
181-
/>
182-
</Flex>
171+
<UsageMeter
172+
label="Monthly free usage"
173+
bucket={usage.sustained}
174+
color={usage.sustained.exceeded ? "red" : undefined}
175+
/>
183176
) : (
184177
<Flex
185178
align="center"

0 commit comments

Comments
 (0)