History

Add 14-day free trial to the Team plan

Adds a 14-day free trial to the $150/mo Team plan and reflects it across the surfaces customers see: - autumn.config.ts: freeTrial on the team plan (14 days, card required, bills automatically when the trial ends). This is the deploy-time source synced via atmn. - marketing pricing: Team card now leads with "14-day free trial, cancel anytime", a "Start free trial" CTA, and a "14-day free trial, then $150 / month" line item. - in-app Choose-a-plan page: reads plan.freeTrial from Autumn, so the Team card shows "14-day free trial, then $150 / month" and a "Start free trial" button while the customer is still trial-eligible. - in-app Billing page: shows a "Free trial" badge and "Trial ends, then billing starts <date>" while a customer is mid-trial.

Rhys Sullivan committed 4 months ago

Commit 60d56ec1 parent f13cdea4 files · +44 −5Browse files

Unified · Split

4 files
  1. apps/cloud/src/routes/app/billing.tsx +9 −3
  2. apps/cloud/src/routes/app/billing_.plans.tsx +26 −1
  3. apps/marketing/src/pages/index.astro +4 −1
  4. autumn.config.ts +5 −0
apps/cloud/src/routes/app/billing.tsx+9−3
@@ -42,6 +42,7 @@ function BillingPage() { · 41 unmodified lines
42 );42 );
43 const isCanceling = activePlan?.customerEligibility?.canceling ?? false;43 const isCanceling = activePlan?.customerEligibility?.canceling ?? false;
44 const isSwitching = isCanceling && scheduledPlan != null;44 const isSwitching = isCanceling && scheduledPlan != null;
45 const isTrialing = activePlan?.customerEligibility?.trialing ?? false;
45 46
46 const displayPlan = isSwitching ? scheduledPlan : activePlan;47 const displayPlan = isSwitching ? scheduledPlan : activePlan;
47 const planId = displayPlan?.id ?? "free";48 const planId = displayPlan?.id ?? "free";
@@ -78,15 +79,20 @@ function BillingPage() { · 30 unmodified lines
78 Canceling79 Canceling
79 </Badge>80 </Badge>
80 )}81 )}
82 {isTrialing && !isCanceling && (
83 <Badge className="bg-primary/10 text-primary">Free trial</Badge>
84 )}
81 </div>85 </div>
82 <p className="mt-1 text-xs text-muted-foreground leading-none">86 <p className="mt-1 text-xs text-muted-foreground leading-none">
83 {isSwitching && sub?.currentPeriodEnd87 {isSwitching && sub?.currentPeriodEnd
84 ? `Starts ${new Date(sub.currentPeriodEnd).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" })}`88 ? `Starts ${new Date(sub.currentPeriodEnd).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" })}`
85 : isCanceling && sub?.currentPeriodEnd89 : isCanceling && sub?.currentPeriodEnd
86 ? `Access until ${new Date(sub.currentPeriodEnd).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" })}`90 ? `Access until ${new Date(sub.currentPeriodEnd).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" })}`
87 : sub?.currentPeriodEnd91 : isTrialing && sub?.currentPeriodEnd
88 ? `Renews ${new Date(sub.currentPeriodEnd).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" })}`92 ? `Trial ends, then billing starts ${new Date(sub.currentPeriodEnd).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" })}`
89 : tagline}93 : sub?.currentPeriodEnd
94 ? `Renews ${new Date(sub.currentPeriodEnd).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" })}`
95 : tagline}
90 </p>96 </p>
91 </div>97 </div>
92 <div className="flex items-center gap-2">98 <div className="flex items-center gap-2">
apps/cloud/src/routes/app/billing_.plans.tsx+26−1
@@ -66,6 +66,12 @@ const ACTION_LABELS: Record<string, string> = { · 65 unmodified lines
66 66
67const PLAN_ORDER = ["free", "team", "enterprise"];67const PLAN_ORDER = ["free", "team", "enterprise"];
68 68
69// "14-day free trial", "1-month free trial", etc. — reads the trial config
70// synced from the repo-root autumn.config.ts so copy tracks the plan. The unit
71// stays singular because it reads as a compound adjective ("14-day", "2-month").
72const trialLabel = (freeTrial: NonNullable<Plan["freeTrial"]>): string =>
73 `${freeTrial.durationLength}-${freeTrial.durationType} free trial`;
74
69function PlansPage() {75function PlansPage() {
70 const { attach, openCustomerPortal, isLoading: customerLoading } = useCustomer();76 const { attach, openCustomerPortal, isLoading: customerLoading } = useCustomer();
71 const { data: plans, isLoading: plansLoading, isFetching } = useListPlans();77 const { data: plans, isLoading: plansLoading, isFetching } = useListPlans();
@@ -127,9 +133,20 @@ function PlansPage() { · 55 unmodified lines
127 const isCanceling = eligibility?.canceling ?? false;133 const isCanceling = eligibility?.canceling ?? false;
128 const isCurrent = status === "active" && !isCanceling;134 const isCurrent = status === "active" && !isCanceling;
129 const isScheduled = status === "scheduled";135 const isScheduled = status === "scheduled";
130 const label = isCanceling ? "Resume" : (ACTION_LABELS[action] ?? "Select");
131 const isUpgradeAction = action === "upgrade" || action === "activate";136 const isUpgradeAction = action === "upgrade" || action === "activate";
132 const isEnterprise = plan.id === "enterprise";137 const isEnterprise = plan.id === "enterprise";
138 // Offer the trial only when the plan defines one and this customer
139 // is still eligible (trialAvailable is false once they've used it).
140 const freeTrial = plan.freeTrial;
141 const trialOffered =
142 freeTrial != null &&
143 eligibility?.trialAvailable !== false &&
144 (action === "activate" || action === "upgrade");
145 const label = isCanceling
146 ? "Resume"
147 : trialOffered
148 ? "Start free trial"
149 : (ACTION_LABELS[action] ?? "Select");
133 150
134 return (151 return (
135 <div152 <div
@@ -179,6 +196,14 @@ function PlansPage() { · 43 unmodified lines
179 )}196 )}
180 </div>197 </div>
181 198
199 {trialOffered && freeTrial && (
200 <p className="mt-2 text-xs font-medium text-primary">
201 {trialLabel(freeTrial)}
202 {plan.price?.amount != null &&
203 `, then $${plan.price.amount} / ${plan.price.interval ?? "month"}`}
204 </p>
205 )}
206
182 <div className="mt-4">207 <div className="mt-4">
183 {(isCurrent && !isCanceling) || isScheduled ? (208 {(isCurrent && !isCanceling) || isScheduled ? (
184 <div className="flex h-9 items-center justify-center rounded-md border border-border bg-muted/30 text-sm font-medium text-muted-foreground">209 <div className="flex h-9 items-center justify-center rounded-md border border-border bg-muted/30 text-sm font-medium text-muted-foreground">
apps/marketing/src/pages/index.astro+4−1
@@ -663,9 +663,12 @@ Source (and the place to start if something breaks): https://github.com/RhysSull · 662 unmodified lines
663 >663 >
664 <span class="text-[13.5px] text-ink-3">/ org / month</span>664 <span class="text-[13.5px] text-ink-3">/ org / month</span>
665 </div>665 </div>
666 <a href="/cloud" class="btn-primary self-start mb-7">Start Team →</a>666 <a href="/cloud" class="btn-primary self-start mb-7"
667 >Start free trial →</a
668 >
667 <ul class="check-list">669 <ul class="check-list">
668 {[670 {[
671 "14-day free trial, then $150 / month",
669 "Unlimited members",672 "Unlimited members",
670 "250,000 included executions per month",673 "250,000 included executions per month",
671 "5 minute execution timeout",674 "5 minute execution timeout",
autumn.config.ts+5−0
@@ -67,6 +67,11 @@ export const team = plan({ · 66 unmodified lines
67 amount: 150,67 amount: 150,
68 interval: "month",68 interval: "month",
69 },69 },
70 freeTrial: {
71 durationLength: 14,
72 durationType: "day",
73 cardRequired: true,
74 },
70 items: [75 items: [
71 item({76 item({
72 featureId: executions.id,77 featureId: executions.id,