From 5a0dffbc4cd1038b4bc45575f2d322710833333d Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Tue, 12 May 2026 05:36:27 +0000 Subject: [PATCH] feat: in-app subscription cancellation for paid plans (#934) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add a "Cancel subscription" button to the billing card for Pro and Scale users, alongside the existing "Manage billing" portal link. - Cancellation is scheduled end-of-cycle via `autumn.updateSubscription({ planId, cancelAction: "cancel_end_of_cycle" })`, so users keep paid features until period end. - Confirmation dialog surfaces remaining days from `useTokenUsage` and the correct plan name (Pro/Scale). - Enterprise users do not see the cancel button — contract-based, portal/sales only. Previously, the only path off a paid plan was Autumn's external customer portal, which contradicted the in-app "cancel anytime from the Billing tab" copy in the support FAQ. --- apps/web/components/settings/account.tsx | 181 ++++++++++++++++++++--- bun.lock | 2 +- 2 files changed, 164 insertions(+), 19 deletions(-) diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index a7ae2d63..676e251a 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -197,6 +197,8 @@ export default function Account() { } = useAuth() const autumn = useCustomer() const [isUpgrading, setIsUpgrading] = useState(false) + const [isCancelling, setIsCancelling] = useState(false) + const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false) const [emailConfirm, setEmailConfirm] = useState("") const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [isClosingAccount, setIsClosingAccount] = useState(false) @@ -287,6 +289,33 @@ export default function Account() { } } + // Enterprise is contract-based — direct those users to the portal/sales. + const cancellablePlanId = + currentPlan === "pro" || currentPlan === "scale" + ? (`api_${currentPlan}` as const) + : null + + const handleCancelSubscription = async () => { + if (!cancellablePlanId) return + setIsCancelling(true) + try { + await autumn.updateSubscription({ + planId: cancellablePlanId, + cancelAction: "cancel_end_of_cycle", + }) + autumn.refetch?.() + setIsCancelDialogOpen(false) + toast.success( + `Subscription cancelled. ${planDisplayNames[currentPlan]} features remain active until the end of your billing period.`, + ) + } catch (error) { + console.error(error) + toast.error("Failed to cancel subscription. Please try again.") + } finally { + setIsCancelling(false) + } + } + const handleDeleteAccount = async () => { if (!user?.email || !emailMatches || membershipsPending) return setIsClosingAccount(true) @@ -546,25 +575,141 @@ export default function Account() {
-