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
4242 );
4343 const isCanceling = activePlan?.customerEligibility?.canceling ?? false;
4444 const isSwitching = isCanceling && scheduledPlan != null;
45+ const isTrialing = activePlan?.customerEligibility?.trialing ?? false;
4546
4647 const displayPlan = isSwitching ? scheduledPlan : activePlan;
4748 const planId = displayPlan?.id ?? "free";
@@ -78,15 +79,20 @@ function BillingPage() { · 30 unmodified lines
7879 Canceling
7980 </Badge>
8081 )}
82+ {isTrialing && !isCanceling && (
83+ <Badge className="bg-primary/10 text-primary">Free trial</Badge>
84+ )}
8185 </div>
8286 <p className="mt-1 text-xs text-muted-foreground leading-none">
8387 {isSwitching && sub?.currentPeriodEnd
8488 ? `Starts ${new Date(sub.currentPeriodEnd).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" })}`
8589 : isCanceling && sub?.currentPeriodEnd
8690 ? `Access until ${new Date(sub.currentPeriodEnd).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" })}`
87− : sub?.currentPeriodEnd
88− ? `Renews ${new Date(sub.currentPeriodEnd).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" })}`
89− : tagline}
91+ : isTrialing && sub?.currentPeriodEnd
92+ ? `Trial ends, then billing starts ${new Date(sub.currentPeriodEnd).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" })}`
93+ : sub?.currentPeriodEnd
94+ ? `Renews ${new Date(sub.currentPeriodEnd).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" })}`
95+ : tagline}
9096 </p>
9197 </div>
9298 <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
6666
6767 const PLAN_ORDER = ["free", "team", "enterprise"];
6868
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").
72+const trialLabel = (freeTrial: NonNullable<Plan["freeTrial"]>): string =>
73+ `${freeTrial.durationLength}-${freeTrial.durationType} free trial`;
74+
6975 function PlansPage() {
7076 const { attach, openCustomerPortal, isLoading: customerLoading } = useCustomer();
7177 const { data: plans, isLoading: plansLoading, isFetching } = useListPlans();
@@ -127,9 +133,20 @@ function PlansPage() { · 55 unmodified lines
127133 const isCanceling = eligibility?.canceling ?? false;
128134 const isCurrent = status === "active" && !isCanceling;
129135 const isScheduled = status === "scheduled";
130− const label = isCanceling ? "Resume" : (ACTION_LABELS[action] ?? "Select");
131136 const isUpgradeAction = action === "upgrade" || action === "activate";
132137 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");
133150
134151 return (
135152 <div
@@ -179,6 +196,14 @@ function PlansPage() { · 43 unmodified lines
179196 )}
180197 </div>
181198
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+
182207 <div className="mt-4">
183208 {(isCurrent && !isCanceling) || isScheduled ? (
184209 <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
663663 >
664664 <span class="text-[13.5px] text-ink-3">/ org / month</span>
665665 </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+ >
667669 <ul class="check-list">
668670 {[
671+ "14-day free trial, then $150 / month",
669672 "Unlimited members",
670673 "250,000 included executions per month",
671674 "5 minute execution timeout",
autumn.config.ts+5−0
@@ -67,6 +67,11 @@ export const team = plan({ · 66 unmodified lines
6767 amount: 150,
6868 interval: "month",
6969 },
70+ freeTrial: {
71+ durationLength: 14,
72+ durationType: "day",
73+ cardRequired: true,
74+ },
7075 items: [
7176 item({
7277 featureId: executions.id,