From f605ab90967a20e429eb7697f4387cfd8b7cbaad Mon Sep 17 00:00:00 2001 From: Charlie Lamb Date: Wed, 4 Feb 2026 17:58:56 +0000 Subject: [PATCH] chore: add advanced settings popover --- .../forms/attach-v2/attachFormSchema.ts | 7 +- .../components/AttachPlanSkeleton.tsx | 30 +++-- .../components/AttachSectionTitle.tsx | 2 + .../components/AttachSettingsPopover.tsx | 122 ++++++++++++++++++ .../components/AttachUpdatesSection.tsx | 61 +++++++-- .../attach-v2/context/AttachFormProvider.tsx | 2 + .../forms/attach-v2/hooks/useAttachForm.ts | 1 + .../attach-v2/hooks/useAttachRequestBody.ts | 8 ++ 8 files changed, 212 insertions(+), 21 deletions(-) create mode 100644 vite/src/components/forms/attach-v2/components/AttachSettingsPopover.tsx diff --git a/vite/src/components/forms/attach-v2/attachFormSchema.ts b/vite/src/components/forms/attach-v2/attachFormSchema.ts index 150f75848..da522e31b 100644 --- a/vite/src/components/forms/attach-v2/attachFormSchema.ts +++ b/vite/src/components/forms/attach-v2/attachFormSchema.ts @@ -1,4 +1,8 @@ -import { FreeTrialDuration, type ProductItem } from "@autumn/shared"; +import { + FreeTrialDuration, + type PlanTiming, + type ProductItem, +} from "@autumn/shared"; import { z } from "zod/v4"; export const AttachFormSchema = z.object({ @@ -9,6 +13,7 @@ export const AttachFormSchema = z.object({ trialLength: z.number().positive().nullable(), trialDuration: z.enum(FreeTrialDuration), trialEnabled: z.boolean(), + planSchedule: z.custom().nullable(), }); export type AttachForm = z.infer; diff --git a/vite/src/components/forms/attach-v2/components/AttachPlanSkeleton.tsx b/vite/src/components/forms/attach-v2/components/AttachPlanSkeleton.tsx index c05a7dee4..a41ce415b 100644 --- a/vite/src/components/forms/attach-v2/components/AttachPlanSkeleton.tsx +++ b/vite/src/components/forms/attach-v2/components/AttachPlanSkeleton.tsx @@ -1,4 +1,4 @@ -import { TimerIcon } from "@phosphor-icons/react"; +import { GearIcon, TimerIcon } from "@phosphor-icons/react"; import { motion } from "motion/react"; import { STAGGER_CONTAINER, @@ -17,21 +17,31 @@ export function AttachPlanSkeleton() { animate="visible" variants={STAGGER_CONTAINER} > - {/* Section title - static content with disabled button */} + {/* Section title - static content with disabled buttons */}

Plan Configuration - } - variant="secondary" - className="h-7 whitespace-nowrap" - disabled - > - Free Trial - + + } + variant="secondary" + className="h-7 whitespace-nowrap" + disabled + > + Settings + + } + variant="secondary" + className="h-7 whitespace-nowrap" + disabled + > + Free Trial + +

diff --git a/vite/src/components/forms/attach-v2/components/AttachSectionTitle.tsx b/vite/src/components/forms/attach-v2/components/AttachSectionTitle.tsx index 308f3c868..a32f786f6 100644 --- a/vite/src/components/forms/attach-v2/components/AttachSectionTitle.tsx +++ b/vite/src/components/forms/attach-v2/components/AttachSectionTitle.tsx @@ -7,6 +7,7 @@ import { } from "@/components/v2/tooltips/Tooltip"; import { cn } from "@/lib/utils"; import { useAttachFormContext } from "../context/AttachFormProvider"; +import { AttachSettingsPopover } from "./AttachSettingsPopover"; export function AttachSectionTitle() { const { hasCustomizations, form, formValues } = useAttachFormContext(); @@ -35,6 +36,7 @@ export function AttachSectionTitle() { )} + { + if (!previewData) return "immediate"; + + const hasOutgoing = previewData.outgoing.length > 0; + if (!hasOutgoing) return "immediate"; + + // Compare prices to determine upgrade vs downgrade + const incomingPrice = previewData.incoming[0]?.plan.price?.amount ?? 0; + const outgoingPrice = previewData.outgoing[0]?.plan.price?.amount ?? 0; + const isUpgrade = incomingPrice > outgoingPrice; + + return isUpgrade ? "immediate" : "end_of_cycle"; + }, [previewData]); + + // Effective value: user's choice or computed default + const effectivePlanSchedule = planSchedule ?? defaultPlanSchedule; + + const handleScheduleChange = (value: PlanTiming) => { + form.setFieldValue("planSchedule", value); + }; + + const isImmediateSelected = effectivePlanSchedule === "immediate"; + const isEndOfCycleSelected = effectivePlanSchedule === "end_of_cycle"; + + // Show blue highlight when user has overridden the default + const hasCustomSchedule = + planSchedule !== null && planSchedule !== defaultPlanSchedule; + + return ( + + + + } + variant="secondary" + className={cn( + "h-7 whitespace-nowrap", + hasCustomSchedule && + "text-blue-400! border-blue-500/50 bg-blue-500/10", + )} + > + Settings + + + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + > +
+
+

+ Advanced Configuration +

+

Override default billing behavior

+
+ +
+ Plan Schedule +
+ } + iconOrientation="left" + variant="secondary" + size="sm" + checked={isImmediateSelected} + onCheckedChange={() => handleScheduleChange("immediate")} + className={cn( + "rounded-r-none", + !isImmediateSelected && "border-r-0", + )} + > + Immediately + + } + iconOrientation="left" + variant="secondary" + size="sm" + checked={isEndOfCycleSelected} + onCheckedChange={() => handleScheduleChange("end_of_cycle")} + className={cn( + "rounded-l-none", + !isEndOfCycleSelected && "border-l-0", + )} + > + End of cycle + +
+
+
+
+
+ ); +} diff --git a/vite/src/components/forms/attach-v2/components/AttachUpdatesSection.tsx b/vite/src/components/forms/attach-v2/components/AttachUpdatesSection.tsx index 65d3180bd..5bd7b12b0 100644 --- a/vite/src/components/forms/attach-v2/components/AttachUpdatesSection.tsx +++ b/vite/src/components/forms/attach-v2/components/AttachUpdatesSection.tsx @@ -1,8 +1,33 @@ import { MinusCircleIcon, PlusCircleIcon } from "@phosphor-icons/react"; +import { motion } from "motion/react"; +import { + STAGGER_CONTAINER, + STAGGER_ITEM, +} from "@/components/forms/update-subscription-v2/constants/animationConstants"; +import { Skeleton } from "@/components/ui/skeleton"; import { SheetSection } from "@/components/v2/sheets/SharedSheetComponents"; import { InfoBox } from "@/views/onboarding2/integrate/components/InfoBox"; import { useAttachFormContext } from "../context/AttachFormProvider"; +function AttachUpdatesSkeleton() { + return ( + + + +
+ + +
+
+
+
+ ); +} + export function AttachUpdatesSection() { const { previewQuery, formValues, product } = useAttachFormContext(); @@ -10,7 +35,15 @@ export function AttachUpdatesSection() { const { data: previewData, isPending } = previewQuery; const outgoing = previewData?.outgoing ?? []; - if (!hasProductSelected || isPending || outgoing.length === 0 || !product) { + if (!hasProductSelected) { + return null; + } + + if (isPending) { + return ; + } + + if (!product) { return null; } @@ -38,15 +71,23 @@ export function AttachUpdatesSection() { return ( - - Attaching{" "} - - {product.name} and - removing {renderOutgoingPlans()} - + + + + Attaching{" "} + + {product.name} + {outgoing.length > 0 && <> and removing {renderOutgoingPlans()}} + + + ); } diff --git a/vite/src/components/forms/attach-v2/context/AttachFormProvider.tsx b/vite/src/components/forms/attach-v2/context/AttachFormProvider.tsx index 74389aa4c..b58a03eab 100644 --- a/vite/src/components/forms/attach-v2/context/AttachFormProvider.tsx +++ b/vite/src/components/forms/attach-v2/context/AttachFormProvider.tsx @@ -95,6 +95,7 @@ export function AttachFormProvider({ trialLength, trialDuration, trialEnabled, + planSchedule, } = formValues; const product = useMemo( @@ -167,6 +168,7 @@ export function AttachFormProvider({ trialLength, trialDuration, trialEnabled, + planSchedule, }); const previewQuery = useAttachPreview({ requestBody }); diff --git a/vite/src/components/forms/attach-v2/hooks/useAttachForm.ts b/vite/src/components/forms/attach-v2/hooks/useAttachForm.ts index 3b0e1ce80..c4dae9a9d 100644 --- a/vite/src/components/forms/attach-v2/hooks/useAttachForm.ts +++ b/vite/src/components/forms/attach-v2/hooks/useAttachForm.ts @@ -18,6 +18,7 @@ export function useAttachForm({ trialLength: null, trialDuration: FreeTrialDuration.Day, trialEnabled: false, + planSchedule: null, } as AttachForm, validators: { onChange: AttachFormSchema, diff --git a/vite/src/components/forms/attach-v2/hooks/useAttachRequestBody.ts b/vite/src/components/forms/attach-v2/hooks/useAttachRequestBody.ts index 3a3f4f88b..84c5c69f7 100644 --- a/vite/src/components/forms/attach-v2/hooks/useAttachRequestBody.ts +++ b/vite/src/components/forms/attach-v2/hooks/useAttachRequestBody.ts @@ -3,6 +3,7 @@ import { type AttachParamsV0Input, type FeatureOptions, type FreeTrialDuration, + type PlanTiming, type ProductItem, ProductItemInterval, type ProductV2, @@ -22,6 +23,7 @@ interface UseAttachRequestBodyParams { trialLength: number | null; trialDuration: FreeTrialDuration; trialEnabled: boolean; + planSchedule: PlanTiming | null; } function convertPrepaidOptionsToFeatureOptions({ @@ -72,6 +74,7 @@ export function useAttachRequestBody({ trialLength, trialDuration, trialEnabled, + planSchedule, }: UseAttachRequestBodyParams) { const requestBody = useMemo((): AttachParamsV0 | null => { if (!customerId || !product) { @@ -118,6 +121,10 @@ export function useAttachRequestBody({ body.free_trial = freeTrial; } + if (planSchedule) { + body.plan_schedule = planSchedule; + } + return body; }, [ customerId, @@ -129,6 +136,7 @@ export function useAttachRequestBody({ trialLength, trialDuration, trialEnabled, + planSchedule, ]); const buildRequestBody = useMemo(