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.
Unified · Split
| @@ -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 | Canceling | 79 | 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?.currentPeriodEnd | 87 | {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?.currentPeriodEnd | 89 | : 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?.currentPeriodEnd | 91 | : 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"> |
| @@ -66,6 +66,12 @@ const ACTION_LABELS: Record<string, string> = { · 65 unmodified lines | |||
| 66 | 66 | ||
| 67 | const PLAN_ORDER = ["free", "team", "enterprise"]; | 67 | const 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"). | ||
| 72 | const trialLabel = (freeTrial: NonNullable<Plan["freeTrial"]>): string => | ||
| 73 | `${freeTrial.durationLength}-${freeTrial.durationType} free trial`; | ||
| 74 | |||
| 69 | function PlansPage() { | 75 | function 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 | <div | 152 | <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"> |
| @@ -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", |
| @@ -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, |