mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-11-09 13:01:56 -10:00
fix(frontend): calmer trial offer, one usage error and clearer limit labels
- Trial billing card: below 80% of the allowance (the meter's Running low line) it keeps the price and start date as a plain card with a secondary "Start Pro now". "Start it now to keep working today" only shows once the trial is running low or used up. The usage card and the offer share one status helper. - Usage that fails to load shows one error in the Usage slot with Try again and Contact support, instead of the generic error card (empty error box, Report Error, Get Help) plus a second error in the side card. The side card stays the normal plan card. - The limit dialog names the reset time once; the "You can continue after" box repeated the headline or description. - Max captions say "already used" instead of "today", which was wrong for weekly limits. - A cancel-pending trial's limit notice and offer say "Subscribe now", matching the plan cards they open. Co-authored-by: Claude Opus 5.5 (Claude Code) <[email protected]> Claude-Session: https://claude.ai/code/session_01StgD1TkCx1ku4bjHh7t4VZ
This commit is contained in:
14 files changed
+321
-52
No files matched your search
+1
-10
@@ -135,7 +135,7 @@ export function RateLimitResetDialog({
|
||||
>
|
||||
{copy.description}
|
||||
</Text>
|
||||
{copy.trialSpent ? (
|
||||
{copy.trialSpent && (
|
||||
<div className="mb-5">
|
||||
<UsageMeter
|
||||
label="Trial allowance"
|
||||
@@ -143,15 +143,6 @@ export function RateLimitResetDialog({
|
||||
detail="One allowance for your trial. It doesn’t refresh."
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
copy.resetLabel && (
|
||||
<div className="mb-5 rounded-xl border border-zinc-200 bg-zinc-50 p-4">
|
||||
<Text variant="small" className="mb-1 !text-zinc-500">
|
||||
You can continue after
|
||||
</Text>
|
||||
<Text variant="body-medium">{copy.resetLabel}</Text>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{isBillingEnabled && model.targetTier && (
|
||||
<PlanOffer
|
||||
|
||||
+12
@@ -135,6 +135,18 @@ describe("Usage limit dialog", () => {
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it("names the reset time once instead of repeating it in a box", () => {
|
||||
render(
|
||||
<RateLimitResetDialog
|
||||
isOpen
|
||||
onClose={vi.fn()}
|
||||
experience={getUsageExperience({ ...usage, tier: "MAX" })}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(/^You can continue /)).toBeDefined();
|
||||
expect(screen.queryByText("You can continue after")).toBeNull();
|
||||
});
|
||||
|
||||
it("pays the open trial invoice straight from the dialog", () => {
|
||||
const onUpgrade = vi.fn();
|
||||
render(
|
||||
|
||||
+20
@@ -211,4 +211,24 @@ describe("Usage notice", () => {
|
||||
}
|
||||
expect(screen.queryByRole("button", { name: /^Start/ })).toBeNull();
|
||||
});
|
||||
it("asks a cancel-pending trial to subscribe instead of starting Pro", async () => {
|
||||
setTrialUser();
|
||||
setup("TRIAL");
|
||||
server.use(
|
||||
http.get("*/api/credits/trial", () =>
|
||||
HttpResponse.json(
|
||||
trialResponse({
|
||||
allowance_used_percent: 100,
|
||||
usage_policy: "lifetime",
|
||||
cancel_at_period_end: true,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
render(<UsageLimitReachedCard />);
|
||||
expect(
|
||||
await screen.findByRole("button", { name: "Subscribe now" }),
|
||||
).toBeDefined();
|
||||
expect(screen.queryByRole("button", { name: /^Start Pro/ })).toBeNull();
|
||||
});
|
||||
});
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { afterEach, beforeEach, expect, it } from "vitest";
|
||||
import { server } from "@/mocks/mock-server";
|
||||
import { render, screen } from "@/tests/integrations/test-utils";
|
||||
import {
|
||||
setTrialUser,
|
||||
trialResponse,
|
||||
} from "@/tests/integrations/trial-fixtures";
|
||||
import { BillingOffer } from "../components/SubscriptionTab/BillingOffer";
|
||||
import { YourPlanCard } from "../components/SubscriptionTab/YourPlanCard/YourPlanCard";
|
||||
|
||||
beforeEach(() => {
|
||||
setTrialUser("billing-user");
|
||||
server.use(
|
||||
http.get("*/api/credits/manage", () =>
|
||||
HttpResponse.json({ url: "https://billing.stripe.com/test" }),
|
||||
),
|
||||
);
|
||||
});
|
||||
afterEach(() => setTrialUser(null));
|
||||
|
||||
function mockTrialAt(percent: number) {
|
||||
server.use(
|
||||
http.get("*/api/credits/subscription", () =>
|
||||
HttpResponse.json({ tier: "TRIAL", monthly_cost: 0 }),
|
||||
),
|
||||
http.get("*/api/credits/trial", () =>
|
||||
HttpResponse.json(
|
||||
trialResponse({
|
||||
usage_policy: "lifetime",
|
||||
allowance_used_percent: percent,
|
||||
}),
|
||||
),
|
||||
),
|
||||
http.get("*/api/chat/usage", () =>
|
||||
HttpResponse.json({ tier: "TRIAL", daily: null, weekly: null }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
it.each([0, 64, 79])(
|
||||
"keeps the price and start date without pushing an early start at %s%%",
|
||||
async (percent) => {
|
||||
mockTrialAt(percent);
|
||||
render(<BillingOffer />);
|
||||
expect(
|
||||
await screen.findByRole("heading", { name: "Pro starts on Sep 17" }),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Your trial keeps going until then. You can also start today.",
|
||||
),
|
||||
).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: "Start Pro now" })).toBeDefined();
|
||||
expect(screen.getByText("$20")).toBeDefined();
|
||||
expect(screen.queryByText(/keep working today/)).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([80, 100])(
|
||||
"offers starting Pro today once the trial is running low at %s%%",
|
||||
async (percent) => {
|
||||
mockTrialAt(percent);
|
||||
render(<BillingOffer />);
|
||||
expect(
|
||||
await screen.findByRole("button", { name: "Start Pro now · $20" }),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Pro starts on its own on Sep 17. Start it now to keep working today.",
|
||||
),
|
||||
).toBeDefined();
|
||||
},
|
||||
);
|
||||
|
||||
it("shows one usage error and keeps the normal plan card when usage fails", async () => {
|
||||
server.use(
|
||||
http.get("*/api/credits/subscription", () =>
|
||||
HttpResponse.json({
|
||||
tier: "PRO",
|
||||
monthly_cost: 5000,
|
||||
tier_costs: { PRO: 5000, MAX: 32000 },
|
||||
tier_multipliers: { PRO: 1, MAX: 8.5 },
|
||||
billing_cycle: "monthly",
|
||||
has_active_stripe_subscription: true,
|
||||
}),
|
||||
),
|
||||
http.get("*/api/credits/trial", () =>
|
||||
HttpResponse.json(trialResponse({ active: false, converted: true })),
|
||||
),
|
||||
http.get("*/api/chat/usage", () => HttpResponse.json({}, { status: 500 })),
|
||||
);
|
||||
render(<YourPlanCard showUsage />);
|
||||
expect(await screen.findByText("We couldn’t load your usage")).toBeDefined();
|
||||
expect(screen.getAllByText("We couldn’t load your usage")).toHaveLength(1);
|
||||
expect(screen.getByRole("button", { name: "Try again" })).toBeDefined();
|
||||
expect(screen.getByRole("link", { name: "Contact support" })).toBeDefined();
|
||||
expect(screen.queryByText("Something went wrong")).toBeNull();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /Report Error|Get Help/ }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
await screen.findByRole("heading", { name: "Upgrade to Max" }),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"What you’ve already used counts toward Max’s higher limit.",
|
||||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
+2
-9
@@ -5,13 +5,13 @@ import { Clock01Icon } from "@hugeicons/core-free-icons";
|
||||
import { Icon } from "@/components/atoms/Icon/Icon";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Skeleton } from "@/components/atoms/Skeleton/Skeleton";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { formatUsagePercent } from "@/services/usageExperience/helpers";
|
||||
import { getSectionMotionProps } from "../../../helpers";
|
||||
import {
|
||||
useAutopilotUsageCard,
|
||||
type UsageWindowView,
|
||||
} from "./useAutopilotUsageCard";
|
||||
import { UsageUnavailable } from "./UsageUnavailable";
|
||||
|
||||
interface Props {
|
||||
index?: number;
|
||||
@@ -25,14 +25,7 @@ export function AutopilotUsageCard({ index = 0 }: Props) {
|
||||
!state.inactive &&
|
||||
(state.isError || (!state.hasUsage && !state.unlimited))
|
||||
)
|
||||
return (
|
||||
<ErrorCard
|
||||
context="usage"
|
||||
hint="Your usage couldn’t be loaded. Try again in a moment."
|
||||
onRetry={state.retry}
|
||||
className="h-full"
|
||||
/>
|
||||
);
|
||||
return <UsageUnavailable onRetry={() => void state.retry()} />;
|
||||
return (
|
||||
<motion.section
|
||||
{...getSectionMotionProps(index, Boolean(reduceMotion))}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { Alert02Icon, RefreshIcon } from "@hugeicons/core-free-icons";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Icon } from "@/components/atoms/Icon/Icon";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { ENTERPRISE_CONTACT_URL } from "../YourPlanCard/useYourPlanCard";
|
||||
|
||||
interface Props {
|
||||
onRetry: () => void;
|
||||
}
|
||||
|
||||
export function UsageUnavailable({ onRetry }: Props) {
|
||||
return (
|
||||
<section
|
||||
aria-label="Usage"
|
||||
className="flex h-full min-h-80 flex-col rounded-2xl border border-zinc-200 bg-white p-6"
|
||||
>
|
||||
<Text variant="large-medium" as="h2">
|
||||
Usage
|
||||
</Text>
|
||||
<div className="my-auto py-8">
|
||||
<span className="mb-4 flex size-9 items-center justify-center rounded-xl bg-zinc-100 text-zinc-600">
|
||||
<Icon icon={Alert02Icon} size={18} />
|
||||
</span>
|
||||
<Text variant="large-medium" className="mb-2">
|
||||
We couldn’t load your usage
|
||||
</Text>
|
||||
<Text variant="small" tone="secondary" className="leading-6">
|
||||
Your plan and your work aren’t affected. Try again in a moment.
|
||||
</Text>
|
||||
<div className="mt-5 flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={onRetry}
|
||||
leftIcon={<Icon icon={RefreshIcon} size={14} aria-hidden="true" />}
|
||||
>
|
||||
Try again
|
||||
</Button>
|
||||
<Button
|
||||
as="NextLink"
|
||||
href={ENTERPRISE_CONTACT_URL}
|
||||
variant="ghost"
|
||||
size="small"
|
||||
>
|
||||
Contact support
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+11
-6
@@ -1,5 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
getFeaturedUsagePercent,
|
||||
getUsageStatus,
|
||||
} from "@/services/usageExperience/helpers";
|
||||
import { useUsageExperience } from "@/services/usageExperience/useUsageExperience";
|
||||
import { formatRelativeReset, formatShortDate } from "../../../helpers";
|
||||
|
||||
@@ -45,13 +49,8 @@ export function useAutopilotUsageCard() {
|
||||
? week
|
||||
: today;
|
||||
const secondary = showTrialWindow ? null : featured === week ? today : week;
|
||||
const percent = featured?.percent ?? 0;
|
||||
const status =
|
||||
percent >= 100
|
||||
? { label: "Allowance reached", className: "text-orange-700" }
|
||||
: percent >= 80
|
||||
? { label: "Running low", className: "text-amber-700" }
|
||||
: { label: "Plenty left", className: "text-zinc-500" };
|
||||
USAGE_STATUS[getUsageStatus(getFeaturedUsagePercent(experience, usage))];
|
||||
return {
|
||||
status,
|
||||
featured,
|
||||
@@ -74,3 +73,9 @@ export function useAutopilotUsageCard() {
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const USAGE_STATUS = {
|
||||
reached: { label: "Allowance reached", className: "text-orange-700" },
|
||||
low: { label: "Running low", className: "text-amber-700" },
|
||||
plenty: { label: "Plenty left", className: "text-zinc-500" },
|
||||
};
|
||||
+22
-17
@@ -7,6 +7,9 @@ import { useUsageExperience } from "@/services/usageExperience/useUsageExperienc
|
||||
import {
|
||||
formatOfferPrice,
|
||||
formatTrialStartDescription,
|
||||
formatTrialStartTitle,
|
||||
getFeaturedUsagePercent,
|
||||
getUsageStatus,
|
||||
} from "@/services/usageExperience/helpers";
|
||||
import { formatCents } from "../../helpers";
|
||||
import {
|
||||
@@ -20,7 +23,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function BillingOffer({ controller }: Props) {
|
||||
const { experience, trial, subscription, isError } = useUsageExperience();
|
||||
const { experience, trial, subscription, usage } = useUsageExperience();
|
||||
const activation = useProActivation();
|
||||
const isTrial = experience.isActiveTrial;
|
||||
const target = isTrial
|
||||
@@ -42,16 +45,6 @@ export function BillingOffer({ controller }: Props) {
|
||||
ready
|
||||
/>
|
||||
);
|
||||
if (isError && experience.tier !== "NO_TIER")
|
||||
return (
|
||||
<SupportOffer
|
||||
eyebrow="Usage"
|
||||
title="We couldn’t load your usage"
|
||||
description="We’re having trouble loading your usage. If it keeps happening, our team can help."
|
||||
label="Contact support"
|
||||
href={ENTERPRISE_CONTACT_URL}
|
||||
/>
|
||||
);
|
||||
if (!canUpgrade || !target)
|
||||
return (
|
||||
<SupportOffer
|
||||
@@ -99,23 +92,35 @@ export function BillingOffer({ controller }: Props) {
|
||||
"Experts for content, sales and research, with Otto coordinating",
|
||||
"Agents that keep working in the background on a schedule",
|
||||
];
|
||||
const title = isTrial
|
||||
? "Start Pro now"
|
||||
: `${experience.tier === "NO_TIER" ? "Start" : "Upgrade to"} ${target}`;
|
||||
const quiet =
|
||||
isTrial &&
|
||||
getUsageStatus(getFeaturedUsagePercent(experience, usage)) === "plenty";
|
||||
const title = quiet
|
||||
? formatTrialStartTitle(experience.trialEndsAt)
|
||||
: isTrial
|
||||
? "Start Pro now"
|
||||
: `${experience.tier === "NO_TIER" ? "Start" : "Upgrade to"} ${target}`;
|
||||
return (
|
||||
<BillingPlanOffer
|
||||
target={target}
|
||||
title={title}
|
||||
quiet={quiet}
|
||||
description={
|
||||
isTrial && experience.freshProUsage
|
||||
? formatTrialStartDescription(experience.trialEndsAt)
|
||||
? quiet
|
||||
? "Your trial keeps going until then. You can also start today."
|
||||
: formatTrialStartDescription(experience.trialEndsAt)
|
||||
: undefined
|
||||
}
|
||||
price={price}
|
||||
isTrialOffer={isTrial}
|
||||
benefits={benefits}
|
||||
actionLabel={
|
||||
isTrial && price ? `${title} · ${price.split(" / ")[0]}` : title
|
||||
quiet
|
||||
? "Start Pro now"
|
||||
: isTrial && price
|
||||
? `${title} · ${price.split(" / ")[0]}`
|
||||
: title
|
||||
}
|
||||
onAction={
|
||||
isTrial
|
||||
@@ -127,7 +132,7 @@ export function BillingOffer({ controller }: Props) {
|
||||
isTrial
|
||||
? "Starting now ends your trial early. You’ll review the charge first."
|
||||
: target === "Max"
|
||||
? "What you’ve used today counts toward Max’s higher limit."
|
||||
? "What you’ve already used counts toward Max’s higher limit."
|
||||
: "Review your options before paying."
|
||||
}
|
||||
/>
|
||||
|
||||
+8
-1
@@ -6,6 +6,7 @@ import { Text } from "@/components/atoms/Text/Text";
|
||||
interface Props {
|
||||
target: string;
|
||||
title: string;
|
||||
quiet?: boolean;
|
||||
description?: string;
|
||||
price: string | null;
|
||||
isTrialOffer?: boolean;
|
||||
@@ -19,6 +20,7 @@ interface Props {
|
||||
export function BillingPlanOffer({
|
||||
target,
|
||||
title,
|
||||
quiet = false,
|
||||
description,
|
||||
price,
|
||||
isTrialOffer = false,
|
||||
@@ -31,7 +33,11 @@ export function BillingPlanOffer({
|
||||
return (
|
||||
<section
|
||||
aria-label={`AutoGPT ${target}`}
|
||||
className="flex h-full flex-col rounded-2xl border border-purple-200 bg-gradient-to-br from-purple-50 via-white to-blue-50 p-6"
|
||||
className={
|
||||
quiet
|
||||
? "flex h-full flex-col rounded-2xl border border-zinc-200 bg-white p-6"
|
||||
: "flex h-full flex-col rounded-2xl border border-purple-200 bg-gradient-to-br from-purple-50 via-white to-blue-50 p-6"
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="flex size-7 items-center justify-center rounded-lg bg-purple-100 text-purple-700">
|
||||
@@ -83,6 +89,7 @@ export function BillingPlanOffer({
|
||||
))}
|
||||
</ul>
|
||||
<Button
|
||||
variant={quiet ? "secondary" : "primary"}
|
||||
size="large"
|
||||
className="mt-auto w-full"
|
||||
onClick={onAction}
|
||||
|
||||
+2
-2
@@ -102,8 +102,8 @@ export function SwitchTierDialog({
|
||||
tone="secondary"
|
||||
className="mt-4 rounded-xl bg-zinc-50 p-4 leading-6"
|
||||
>
|
||||
You can keep going once payment goes through. What you’ve used today
|
||||
counts toward Max’s limit
|
||||
You can keep going once payment goes through. What you’ve already
|
||||
used counts toward Max’s limit
|
||||
{usageMultiplier
|
||||
? `, which is ${usageMultiplier} times as high as Pro’s.`
|
||||
: "."}
|
||||
|
||||
@@ -122,7 +122,7 @@ export function PlanOffer({
|
||||
{freshUsage
|
||||
? "You’ll see the full charge before you pay."
|
||||
: tier === "MAX"
|
||||
? "What you’ve used today counts toward Max’s higher limit."
|
||||
? "What you’ve already used counts toward Max’s higher limit."
|
||||
: "Review your plan and payment before confirming."}
|
||||
</Text>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getUsageExperience, formatUsageDate } from "../helpers";
|
||||
import {
|
||||
getFeaturedUsagePercent,
|
||||
getUsageExperience,
|
||||
getUsageOfferAction,
|
||||
getUsageStatus,
|
||||
formatUsageDate,
|
||||
} from "../helpers";
|
||||
|
||||
const usage = {
|
||||
tier: "PRO",
|
||||
@@ -134,6 +140,22 @@ describe("usage experience", () => {
|
||||
});
|
||||
},
|
||||
);
|
||||
it("asks a cancel-pending trial to subscribe rather than start Pro", () => {
|
||||
const experience = getUsageExperience(
|
||||
{ ...usage, tier: "TRIAL" },
|
||||
{ ...trial, offer: { tier: "PRO" }, cancel_at_period_end: true },
|
||||
);
|
||||
expect(experience.isCancelPending).toBe(true);
|
||||
expect(getUsageOfferAction(experience, {})).toBe("Subscribe now");
|
||||
});
|
||||
it("keeps Start Pro for a trial that ended without a pending cancellation", () => {
|
||||
const experience = getUsageExperience(
|
||||
{ ...usage, tier: "NO_TIER" },
|
||||
{ ...trial, active: false, status: "expired" },
|
||||
);
|
||||
expect(experience.isCancelPending).toBe(false);
|
||||
expect(getUsageOfferAction(experience, {})).toBe("Start Pro");
|
||||
});
|
||||
it("carries the open invoice only for a failed first charge", () => {
|
||||
const payment_due = {
|
||||
amount_due: 5000,
|
||||
@@ -159,4 +181,34 @@ describe("usage experience", () => {
|
||||
).payment,
|
||||
).toBeNull();
|
||||
});
|
||||
it.each([
|
||||
[0, "plenty"],
|
||||
[79.9, "plenty"],
|
||||
[80, "low"],
|
||||
[99.9, "low"],
|
||||
[100, "reached"],
|
||||
] as const)("reports %s%% as %s", (percent, status) => {
|
||||
expect(getUsageStatus(percent)).toBe(status);
|
||||
});
|
||||
it("features the same window the usage card shows", () => {
|
||||
const lifetime = getUsageExperience(
|
||||
{ ...usage, tier: "TRIAL" },
|
||||
{ ...trial, allowance_used_percent: 64 },
|
||||
);
|
||||
expect(getFeaturedUsagePercent(lifetime, usage)).toBe(64);
|
||||
const weekly = {
|
||||
...usage,
|
||||
daily: { ...usage.daily, percent_used: 40 },
|
||||
weekly: { ...usage.weekly, percent_used: 100 },
|
||||
};
|
||||
expect(getFeaturedUsagePercent(getUsageExperience(weekly), weekly)).toBe(
|
||||
100,
|
||||
);
|
||||
const daily = {
|
||||
...usage,
|
||||
daily: { ...usage.daily, percent_used: 30 },
|
||||
weekly: { ...usage.weekly, percent_used: 70 },
|
||||
};
|
||||
expect(getFeaturedUsagePercent(getUsageExperience(daily), daily)).toBe(30);
|
||||
});
|
||||
});
|
||||
@@ -83,6 +83,7 @@ export function getUsageExperience(
|
||||
isActiveTrial &&
|
||||
!trial?.cancel_at_period_end &&
|
||||
trial?.offer?.tier === "PRO",
|
||||
isCancelPending: isActiveTrial && trial?.cancel_at_period_end === true,
|
||||
trialPercent,
|
||||
inactiveTrialStatus,
|
||||
payment:
|
||||
@@ -106,6 +107,27 @@ export function getUsageExperience(
|
||||
|
||||
export type UsageExperience = ReturnType<typeof getUsageExperience>;
|
||||
|
||||
export function getUsageStatus(percent: number) {
|
||||
if (percent >= 100) return "reached" as const;
|
||||
return percent >= 80 ? ("low" as const) : ("plenty" as const);
|
||||
}
|
||||
|
||||
export function getFeaturedUsagePercent(
|
||||
experience: UsageExperience,
|
||||
usage?: Usage | null,
|
||||
) {
|
||||
if (
|
||||
experience.isLifetimeTrial ||
|
||||
(experience.isActiveTrial && experience.window === "trial")
|
||||
)
|
||||
return experience.trialPercent ?? 0;
|
||||
const featured =
|
||||
experience.window === "weekly" || !usage?.daily
|
||||
? usage?.weekly
|
||||
: usage.daily;
|
||||
return featured?.percent_used ?? 0;
|
||||
}
|
||||
|
||||
export function formatUsagePercent(value: number) {
|
||||
if (value > 0 && value < 1) return "<1%";
|
||||
if (value < 100 && value > 99) return "99%";
|
||||
@@ -139,6 +161,11 @@ export function formatTrialStartDescription(value: string | Date | null) {
|
||||
return `Pro starts on its own ${date ? `on ${date}` : "when your trial ends"}. Start it now to keep working today.`;
|
||||
}
|
||||
|
||||
export function formatTrialStartTitle(value: string | Date | null) {
|
||||
const date = formatTrialDate(value);
|
||||
return date ? `Pro starts on ${date}` : "Pro starts when your trial ends";
|
||||
}
|
||||
|
||||
export function formatResumeTime(value: string | Date | null) {
|
||||
if (!value) return null;
|
||||
const date = new Date(value);
|
||||
@@ -193,5 +220,6 @@ export function getUsageOfferAction(
|
||||
const amount = formatOfferPrice(offer.price, offer.currency);
|
||||
return `Start Pro now${amount ? ` · ${amount}` : ""}`;
|
||||
}
|
||||
if (experience.isCancelPending) return "Subscribe now";
|
||||
return experience.tier === "BASIC" ? "Upgrade to Pro" : "Start Pro";
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
formatResumeTime,
|
||||
formatTrialDate,
|
||||
formatTrialStartDescription,
|
||||
formatUsageDate,
|
||||
type UsageExperience,
|
||||
} from "./helpers";
|
||||
import { formatPaymentDue } from "./payment";
|
||||
@@ -35,7 +34,6 @@ export function usagePresentation(
|
||||
: canceled
|
||||
? "Your trial won’t turn into a paid subscription. Your chats, agents and results are saved whenever you’re ready to choose a plan."
|
||||
: "Your chats, agents and results are saved. Choose a plan to keep going.",
|
||||
resetLabel: null,
|
||||
supportLabel: "Contact support",
|
||||
secondary: "Back to my work",
|
||||
trialSpent: false,
|
||||
@@ -68,9 +66,6 @@ export function usagePresentation(
|
||||
: tier === "PRO"
|
||||
? `${resume}, or upgrade to Max for ${usageMultiplier ? `${usageMultiplier} times the usage` : "more usage"}.`
|
||||
: `${resume}, or ${tier === "BASIC" ? "upgrade to Pro" : "start Pro"}.`,
|
||||
resetLabel: experience.resetsAt
|
||||
? formatUsageDate(experience.resetsAt)
|
||||
: null,
|
||||
supportLabel:
|
||||
tier === "ENTERPRISE"
|
||||
? "Contact your account team"
|
||||
|
||||
Reference in new issue
Block a user