From 9a1bab718292310bca69e5694fc1ef2096377c82 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Wed, 10 Jun 2026 01:04:33 -0700 Subject: [PATCH] feat(billing): cancel flow with reason capture + book-a-call (#1077) Co-authored-by: ved015 --- apps/web/components/settings/billing.tsx | 315 +++++++++++++----- .../web/components/settings/cancel-reasons.ts | 19 ++ 2 files changed, 254 insertions(+), 80 deletions(-) create mode 100644 apps/web/components/settings/cancel-reasons.ts diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index 2021f83b..3627b220 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -11,8 +11,15 @@ import { DialogContent, DialogTrigger, } from "@ui/components/dialog" +import { Logo } from "@ui/assets/Logo" import { useQuery, useQueryClient } from "@tanstack/react-query" import { useCustomer } from "autumn-js/react" +import { usePostHog } from "@lib/posthog" +import { + CANCEL_REASONS, + cancelReasonNeedsDetail, + type CancelReasonValue, +} from "./cancel-reasons" import { Check, ChevronLeft, @@ -31,6 +38,38 @@ import { toast } from "sonner" const API_BASE = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" +const BOOK_CALL_HREF = "https://cal.com/maheshthedev/15min" + +function GoogleMeetIcon({ className }: { className?: string }) { + return ( + + ) +} + const CREDIT_FEATURE_ID = "usd_credits" const TOP_UP_PLAN_ID = "credits_topup" const TOP_UP_AMOUNTS = [10, 25, 50, 100] as const @@ -437,11 +476,15 @@ export default function Billing() { const queryClient = useQueryClient() const { user, org } = useAuth() const autumn = useCustomer() + const posthog = usePostHog() const [isUpgrading, setIsUpgrading] = useState(false) const [isCancelling, setIsCancelling] = useState(false) const [isResuming, setIsResuming] = useState(false) const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false) - const [cancelConfirmText, setCancelConfirmText] = useState("") + const [cancelReason, setCancelReason] = useState( + null, + ) + const [cancelDetail, setCancelDetail] = useState("") const [isCreditsDialogOpen, setIsCreditsDialogOpen] = useState(false) const [isPlanCarouselActive, setIsPlanCarouselActive] = useState(false) const [planPage, setPlanPage] = useState<0 | 1>(0) @@ -578,16 +621,11 @@ export default function Billing() { ? (`api_${currentPlan}` as const) : null - const currentPlanCard = [...PLAN_CARDS, ...ADVANCED_PLAN_CARDS].find( - (p) => p.id === currentPlan, - ) - const cancelLossItems = currentPlanCard - ? [ - `${currentPlanCard.credits}/mo included credits`, - ...currentPlanCard.features.filter((f) => !/credit/i.test(f)), - ] - : [] - const canConfirmCancel = cancelConfirmText.trim().toUpperCase() === "CANCEL" + const cancelNeedsDetail = + cancelReason != null && cancelReasonNeedsDetail(cancelReason) + const canConfirmCancel = + cancelReason != null && + (!cancelNeedsDetail || cancelDetail.trim().length > 0) const canceledSub = getCanceledSubscription(autumn.data?.subscriptions) const isPlanCanceling = canceledSub != null @@ -627,6 +665,11 @@ export default function Billing() { } } + const resetCancelForm = () => { + setCancelReason(null) + setCancelDetail("") + } + const handleCancelSubscription = async () => { if (!cancellablePlanId) return setIsCancelling(true) @@ -635,9 +678,18 @@ export default function Billing() { planId: cancellablePlanId, cancelAction: "cancel_end_of_cycle", }) + if (posthog?.__loaded) { + posthog.capture("subscription_cancelled", { + reason: cancelReason, + reason_detail: cancelDetail.trim() || null, + plan: currentPlan, + plan_id: cancellablePlanId, + surface: "nova", + }) + } autumn.refetch?.() setIsCancelDialogOpen(false) - setCancelConfirmText("") + resetCancelForm() toast.success( `Subscription cancelled. ${planDisplayNames[currentPlan]} features remain active until the end of your billing period.`, ) @@ -934,7 +986,7 @@ export default function Billing() { open={isCancelDialogOpen} onOpenChange={(open) => { setIsCancelDialogOpen(open) - if (!open) setCancelConfirmText("") + if (!open) resetCancelForm() }} > @@ -950,7 +1002,7 @@ export default function Billing() {
@@ -985,74 +1037,177 @@ export default function Billing() {
- {cancelLossItems.length > 0 ? ( -
-

- You'll lose -

-
    - {cancelLossItems.map((item) => ( -
  • +
    +
    + +
    +
    + +

    + Talk to us first +

    +
    +

    - - {item} -

  • - ))} -
+ Give us 15 minutes before you go — a quick call + with the team where we'll: +

+
    + {[ + "Fix anything that's not working for you", + "Help you integrate Supermemory the right way", + "Get a discount to stay", + ].map((benefit) => ( +
  • + + {benefit} +
  • + ))} +
+ { + if (posthog?.__loaded) { + posthog.capture( + "cancel_book_call_clicked", + { + plan: currentPlan, + surface: "nova", + }, + ) + } + }} + className={cn( + dmSans125ClassName(), + "group mt-auto inline-flex h-10 items-center justify-center gap-1.5 rounded-[10px] bg-[#0054AD] px-4 text-[13px] font-semibold text-white transition-colors hover:bg-[#0B65C9]", + )} + > + Book a call + + +
+
+ +
+ + or + +
+ +
+

+ Why are you leaving? +

+
+ {CANCEL_REASONS.map((option) => { + const selected = cancelReason === option.value + return ( + + ) + })} +
+ {cancelReason !== null ? ( +
+ +