From d4907e63f2c837f68d9e53655a03bd072f3dcb04 Mon Sep 17 00:00:00 2001 From: Ayush Rodrigues Date: Fri, 24 Apr 2026 17:01:54 +0100 Subject: [PATCH] feat: smart default scope for attach and create-schedule sheets Adds useSheetScopeEntityId hook used by both AttachProductSheet and CreateScheduleSheet, which seeds scope from ?entity_id= and falls back to the customer's active entity-level plan when no customer-level plan exists. AttachFormProvider now exposes entityId and onScopeChange on context, so AttachProductSelection and the scope dropdown pull from the form context instead of the page-level useEntity() store. Made-with: Cursor --- .../setupCreateScheduleBillingContext.ts | 52 +++++++++++++- .../createSchedule/createScheduleParamsV0.ts | 10 +++ .../components/AttachProductSelection.tsx | 4 +- .../attach-v2/context/AttachFormProvider.tsx | 9 +++ .../CreateScheduleAdvancedSection.tsx | 67 +++++++++++++++++++ .../components/CreateScheduleSheetContent.tsx | 2 + .../context/CreateScheduleFormProvider.tsx | 14 ++++ .../createScheduleFormSchema.ts | 4 +- .../hooks/useCreateScheduleForm.ts | 2 + .../hooks/useCreateScheduleRequestBody.ts | 50 +++++++++++++- .../PlanEditorContext.tsx | 5 -- vite/src/hooks/useSheetScopeEntityId.ts | 34 ++++++++++ .../components/sheets/AttachProductSheet.tsx | 16 +++-- .../components/sheets/CreateScheduleSheet.tsx | 16 ++--- 14 files changed, 259 insertions(+), 26 deletions(-) create mode 100644 vite/src/components/forms/create-schedule/components/CreateScheduleAdvancedSection.tsx create mode 100644 vite/src/hooks/useSheetScopeEntityId.ts diff --git a/server/src/internal/billing/v2/actions/createSchedule/setup/setupCreateScheduleBillingContext.ts b/server/src/internal/billing/v2/actions/createSchedule/setup/setupCreateScheduleBillingContext.ts index aba87f078..13ba640be 100644 --- a/server/src/internal/billing/v2/actions/createSchedule/setup/setupCreateScheduleBillingContext.ts +++ b/server/src/internal/billing/v2/actions/createSchedule/setup/setupCreateScheduleBillingContext.ts @@ -7,9 +7,13 @@ import { type MultiAttachParamsV0, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { setupAnchorResetRefund } from "@/internal/billing/v2/setup/setupAnchorResetRefund"; +import { setupBillingCycleAnchor } from "@/internal/billing/v2/setup/setupBillingCycleAnchor"; +import { setupResetCycleAnchor } from "@/internal/billing/v2/setup/setupResetCycleAnchor"; import { setupImmediateMultiProductBillingContext } from "../../common/immediateMultiProduct/setupImmediateMultiProductBillingContext"; import { normalizeCreateSchedulePhases } from "../errors/normalizeCreateSchedulePhases"; import { validateCreateSchedulePhasePlans } from "../errors/validateCreateSchedulePhasePlans"; +import { billingContextToRecurringAndScheduled } from "../utils/billingContextToRecurringAndScheduled"; import { setupScheduledProductsContext } from "./setupScheduledProductsContext"; type CreateScheduleCheckoutModeContext = Pick< @@ -115,7 +119,7 @@ export const setupCreateScheduleBillingContext = async ({ ), ); - return { + const scheduleBillingContext: CreateScheduleBillingContext = { ...billingContext, checkoutMode: setupCreateScheduleCheckoutMode({ billingContext, @@ -133,8 +137,54 @@ export const setupCreateScheduleBillingContext = async ({ billingContext.isCustom || scheduledCustomPrices.length > 0 || scheduledCustomEntitlements.length > 0, + requestedProrationBehavior: params.billing_behavior, + requestedBillingCycleAnchor: params.billing_cycle_anchor, immediatePhase, futurePhases, scheduledPhaseContexts, }; + + const { recurringActive } = billingContextToRecurringAndScheduled({ + billingContext: scheduleBillingContext, + }); + + // setupImmediateMultiProductBillingContext does not forward + // `billing_cycle_anchor`, so billingCycleAnchorMs still reflects the existing + // Stripe anchor. When the caller asks to reset the cycle we must recompute + // the anchor (and the reset-cycle anchor) so downstream proration math runs + // against the new `[now, now + interval]` period. Mirrors the attach / + // updateSubscription setups. + if (params.billing_cycle_anchor !== undefined) { + const firstProduct = billingContext.fullProducts[0]; + if (firstProduct) { + let recomputedAnchor = setupBillingCycleAnchor({ + stripeSubscription: billingContext.stripeSubscription, + customerProduct: recurringActive[0], + newFullProduct: firstProduct, + trialContext: billingContext.trialContext, + currentEpochMs: billingContext.currentEpochMs, + requestedBillingCycleAnchor: params.billing_cycle_anchor, + }); + if (billingContext.trialContext?.trialEndsAt) { + recomputedAnchor = billingContext.trialContext.trialEndsAt; + } + scheduleBillingContext.billingCycleAnchorMs = recomputedAnchor; + scheduleBillingContext.resetCycleAnchorMs = setupResetCycleAnchor({ + billingCycleAnchorMs: recomputedAnchor, + customerProduct: undefined, + newFullProduct: firstProduct, + }); + } + } + + // Keep forward-looking charges (e.g. prepaid renewals) when the caller asks + // to reset the cycle with proration off; without this, finalizeLineItems + // drops every line item and total due now collapses to 0. + scheduleBillingContext.anchorResetRefund = setupAnchorResetRefund({ + billingCycleAnchor: params.billing_cycle_anchor, + prorationBehavior: params.billing_behavior, + outgoingCustomerProduct: recurringActive[0], + }); + + return scheduleBillingContext; }; diff --git a/shared/api/billing/createSchedule/createScheduleParamsV0.ts b/shared/api/billing/createSchedule/createScheduleParamsV0.ts index 51e9eabac..f9f15851a 100644 --- a/shared/api/billing/createSchedule/createScheduleParamsV0.ts +++ b/shared/api/billing/createSchedule/createScheduleParamsV0.ts @@ -4,6 +4,8 @@ import { RedirectModeSchema } from "@api/billing/common/redirectMode"; import { BasePriceParamsSchema } from "@api/products/components/basePrice/basePrice"; import { CreatePlanItemParamsV1Schema } from "@api/products/items/crud/createPlanItemParamsV1"; import { z } from "zod/v4"; +import { BillingBehaviorSchema } from "../common/billingBehavior"; +import { BillingCycleAnchorSchema } from "../common/billingCycleAnchor"; const CreateScheduleCustomizePlanSchema = z .object({ @@ -81,6 +83,14 @@ export const CreateScheduleParamsV0Schema = z description: "Controls when to return a checkout URL for the immediate phase. 'always' forces a confirmation or checkout flow, 'if_required' only redirects when needed, and 'never' disables redirects.", }), + billing_behavior: BillingBehaviorSchema.optional().meta({ + description: + "Whether to prorate the immediate phase. 'none' skips proration charges and credits.", + }), + billing_cycle_anchor: BillingCycleAnchorSchema.optional().meta({ + description: + "Pass 'now' to reset the billing cycle anchor of the immediate phase to the current time.", + }), phases: z .tuple([CreateSchedulePhaseSchema]) .rest(CreateSchedulePhaseSchema) diff --git a/vite/src/components/forms/attach-v2/components/AttachProductSelection.tsx b/vite/src/components/forms/attach-v2/components/AttachProductSelection.tsx index bebe1c673..59a228a1a 100644 --- a/vite/src/components/forms/attach-v2/components/AttachProductSelection.tsx +++ b/vite/src/components/forms/attach-v2/components/AttachProductSelection.tsx @@ -3,17 +3,15 @@ import { isProductCurrentlyAttached, } from "@autumn/shared"; import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; -import { useEntity } from "@/hooks/stores/useSubscriptionStore"; import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery"; import { useAttachFormContext } from "../context/AttachFormProvider"; export function AttachProductSelection() { - const { form, hasCustomizations } = useAttachFormContext(); + const { form, hasCustomizations, entityId } = useAttachFormContext(); const { products } = useProductsQuery(); const availableProducts = products.filter((p) => !p.archived); const { customer } = useCusQuery(); - const { entityId } = useEntity(); const productId = form.state.values.productId; diff --git a/vite/src/components/forms/attach-v2/context/AttachFormProvider.tsx b/vite/src/components/forms/attach-v2/context/AttachFormProvider.tsx index ca992b98c..c38491e8f 100644 --- a/vite/src/components/forms/attach-v2/context/AttachFormProvider.tsx +++ b/vite/src/components/forms/attach-v2/context/AttachFormProvider.tsx @@ -59,6 +59,9 @@ interface AttachFormContextValue { formValues: AttachForm; features: Feature[]; + entityId: string | undefined; + onScopeChange?: (entityId: string | undefined) => void; + product: ProductV2 | undefined; prepaidItems: PrepaidItemWithFeature[]; originalItems: ProductItem[] | undefined; @@ -103,6 +106,7 @@ interface AttachFormProviderProps { onPlanEditorClose?: () => void; onCheckoutRedirect?: (checkoutUrl: string) => void; onSuccess?: () => void; + onScopeChange?: (entityId: string | undefined) => void; initialSchedulePlan?: SchedulePlan | null; disablePreview?: boolean; children: ReactNode; @@ -135,6 +139,7 @@ export function AttachFormProvider({ onPlanEditorClose, onCheckoutRedirect, onSuccess, + onScopeChange, initialSchedulePlan, disablePreview, children, @@ -472,6 +477,8 @@ export function AttachFormProvider({ form, formValues, features, + entityId, + onScopeChange, product: effectiveProduct, prepaidItems, originalItems, @@ -496,6 +503,8 @@ export function AttachFormProvider({ form, formValues, features, + entityId, + onScopeChange, effectiveProduct, prepaidItems, originalItems, diff --git a/vite/src/components/forms/create-schedule/components/CreateScheduleAdvancedSection.tsx b/vite/src/components/forms/create-schedule/components/CreateScheduleAdvancedSection.tsx new file mode 100644 index 000000000..241e554a4 --- /dev/null +++ b/vite/src/components/forms/create-schedule/components/CreateScheduleAdvancedSection.tsx @@ -0,0 +1,67 @@ +import type { ReactNode } from "react"; +import { + AdvancedSection, + ConfigRow, +} from "@/components/forms/shared/advanced-section"; +import { Switch } from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/v2/tooltips/Tooltip"; +import { useCreateScheduleFormContext } from "../context/CreateScheduleFormProvider"; + +export function CreateScheduleAdvancedSection() { + const { form, formValues } = useCreateScheduleFormContext(); + const { billingBehavior, resetBillingCycle, phases } = formValues; + + const isProrate = billingBehavior !== "none"; + const hasMultipleImmediatePlans = (phases[0]?.plans.length ?? 0) > 1; + const disabledReason = hasMultipleImmediatePlans + ? "Not yet supported for multi attach" + : null; + + const renderToggle = ({ + checked, + onCheckedChange, + }: { + checked: boolean; + onCheckedChange: (checked: boolean) => void; + }): ReactNode => ( + + + + + + + {disabledReason && {disabledReason}} + + ); + + return ( + + + form.setFieldValue("billingBehavior", checked ? null : "none"), + })} + /> + + form.setFieldValue("resetBillingCycle", !!checked), + })} + /> + + ); +} diff --git a/vite/src/components/forms/create-schedule/components/CreateScheduleSheetContent.tsx b/vite/src/components/forms/create-schedule/components/CreateScheduleSheetContent.tsx index 1083aa403..c4aee4977 100644 --- a/vite/src/components/forms/create-schedule/components/CreateScheduleSheetContent.tsx +++ b/vite/src/components/forms/create-schedule/components/CreateScheduleSheetContent.tsx @@ -18,6 +18,7 @@ import { cn } from "@/lib/utils"; import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery"; import { useCreateScheduleFormContext } from "../context/CreateScheduleFormProvider"; import { useHasSchedule } from "../hooks/useHasSchedule"; +import { CreateScheduleAdvancedSection } from "./CreateScheduleAdvancedSection"; import { SchedulePhaseCard } from "./SchedulePhaseCard"; import { SchedulePreview } from "./SchedulePreview"; @@ -182,6 +183,7 @@ export function CreateScheduleReviewContent() { />
+
diff --git a/vite/src/components/forms/create-schedule/context/CreateScheduleFormProvider.tsx b/vite/src/components/forms/create-schedule/context/CreateScheduleFormProvider.tsx index 5a062d0e7..b55cfab96 100644 --- a/vite/src/components/forms/create-schedule/context/CreateScheduleFormProvider.tsx +++ b/vite/src/components/forms/create-schedule/context/CreateScheduleFormProvider.tsx @@ -140,6 +140,16 @@ export function CreateScheduleFormProvider({ [form.store], ); + const getBillingBehavior = useCallback( + () => form.store.state.values.billingBehavior ?? null, + [form.store], + ); + + const getResetBillingCycle = useCallback( + () => form.store.state.values.resetBillingCycle ?? false, + [form.store], + ); + const buildRequestBody = useBuildCreateScheduleRequestBody({ customerId, entityId, @@ -147,6 +157,8 @@ export function CreateScheduleFormProvider({ features, nowMs, getPhases, + getBillingBehavior, + getResetBillingCycle, }); const previewRequestBody = useCreateScheduleRequestBody({ @@ -156,6 +168,8 @@ export function CreateScheduleFormProvider({ products, features, nowMs, + billingBehavior: formValues.billingBehavior, + resetBillingCycle: formValues.resetBillingCycle, }); const phaseTimingError = useMemo( diff --git a/vite/src/components/forms/create-schedule/createScheduleFormSchema.ts b/vite/src/components/forms/create-schedule/createScheduleFormSchema.ts index 971594220..ff4952834 100644 --- a/vite/src/components/forms/create-schedule/createScheduleFormSchema.ts +++ b/vite/src/components/forms/create-schedule/createScheduleFormSchema.ts @@ -1,4 +1,4 @@ -import type { ProductItem } from "@autumn/shared"; +import { BillingBehaviorSchema, type ProductItem } from "@autumn/shared"; import { z } from "zod/v4"; export const SchedulePlanSchema = z.object({ @@ -169,6 +169,8 @@ export function getPhaseTimingError({ export const CreateScheduleFormSchema = z .object({ phases: z.array(SchedulePhaseSchema).min(1), + billingBehavior: BillingBehaviorSchema.nullable(), + resetBillingCycle: z.boolean(), }) .refine( (data) => diff --git a/vite/src/components/forms/create-schedule/hooks/useCreateScheduleForm.ts b/vite/src/components/forms/create-schedule/hooks/useCreateScheduleForm.ts index fb1417898..a8d231b5d 100644 --- a/vite/src/components/forms/create-schedule/hooks/useCreateScheduleForm.ts +++ b/vite/src/components/forms/create-schedule/hooks/useCreateScheduleForm.ts @@ -13,6 +13,8 @@ export function useCreateScheduleForm({ } = {}) { const defaultValues: CreateScheduleForm = initialValues ?? { phases: [{ startsAt: null, plans: [{ ...EMPTY_SCHEDULE_PLAN }] }], + billingBehavior: null, + resetBillingCycle: false, }; const initialValuesRef = useRef(defaultValues); diff --git a/vite/src/components/forms/create-schedule/hooks/useCreateScheduleRequestBody.ts b/vite/src/components/forms/create-schedule/hooks/useCreateScheduleRequestBody.ts index 3a96842f0..701b52b0e 100644 --- a/vite/src/components/forms/create-schedule/hooks/useCreateScheduleRequestBody.ts +++ b/vite/src/components/forms/create-schedule/hooks/useCreateScheduleRequestBody.ts @@ -1,5 +1,6 @@ import type { ApiPlanItemV1, + BillingBehavior, CreateScheduleParamsV0, Feature, ProductItem, @@ -110,6 +111,8 @@ export function buildCreateScheduleRequestBody({ products, features, nowMs, + billingBehavior, + resetBillingCycle, }: { customerId: string | undefined; entityId: string | undefined; @@ -117,6 +120,8 @@ export function buildCreateScheduleRequestBody({ products: ProductV2[]; features: Feature[]; nowMs?: number; + billingBehavior?: BillingBehavior | null; + resetBillingCycle?: boolean; }): CreateScheduleParamsV0 | null { const now = nowMs ?? Date.now(); if (!customerId || phases.length === 0) return null; @@ -164,6 +169,17 @@ export function buildCreateScheduleRequestBody({ phases: validPhases, }; if (entityId) body.entity_id = entityId; + + // `billing_behavior` / `billing_cycle_anchor` aren't supported when the + // immediate phase is a multi-attach. The review UI disables the toggles in + // that case; mirror the same guard here so stale values don't leak into the + // request if the user flips from single-plan to multi-plan after toggling. + const immediatePlanCount = validPhases[0]?.plans.length ?? 0; + const supportsBillingFlags = immediatePlanCount === 1; + if (supportsBillingFlags) { + if (billingBehavior) body.billing_behavior = billingBehavior; + if (resetBillingCycle) body.billing_cycle_anchor = "now"; + } return body as CreateScheduleParamsV0; } @@ -174,6 +190,8 @@ export function useCreateScheduleRequestBody({ products, features, nowMs, + billingBehavior, + resetBillingCycle, }: { customerId: string | undefined; entityId: string | undefined; @@ -181,6 +199,8 @@ export function useCreateScheduleRequestBody({ products: ProductV2[]; features: Feature[]; nowMs?: number; + billingBehavior?: BillingBehavior | null; + resetBillingCycle?: boolean; }) { return useMemo( () => @@ -191,8 +211,19 @@ export function useCreateScheduleRequestBody({ products, features, nowMs, + billingBehavior, + resetBillingCycle, }), - [customerId, entityId, phases, products, features, nowMs], + [ + customerId, + entityId, + phases, + products, + features, + nowMs, + billingBehavior, + resetBillingCycle, + ], ); } @@ -203,6 +234,8 @@ export function useBuildCreateScheduleRequestBody({ features, nowMs, getPhases, + getBillingBehavior, + getResetBillingCycle, }: { customerId: string | undefined; entityId: string | undefined; @@ -210,6 +243,8 @@ export function useBuildCreateScheduleRequestBody({ features: Feature[]; nowMs?: number; getPhases: () => SchedulePhase[]; + getBillingBehavior?: () => BillingBehavior | null; + getResetBillingCycle?: () => boolean; }) { return useMemo( () => @@ -229,6 +264,8 @@ export function useBuildCreateScheduleRequestBody({ products, features, nowMs, + billingBehavior: getBillingBehavior?.() ?? null, + resetBillingCycle: getResetBillingCycle?.() ?? false, }); if (!requestBody) return null; @@ -246,6 +283,15 @@ export function useBuildCreateScheduleRequestBody({ return requestBody; }, - [customerId, entityId, products, features, nowMs, getPhases], + [ + customerId, + entityId, + products, + features, + nowMs, + getPhases, + getBillingBehavior, + getResetBillingCycle, + ], ); } diff --git a/vite/src/components/v2/inline-custom-plan-editor/PlanEditorContext.tsx b/vite/src/components/v2/inline-custom-plan-editor/PlanEditorContext.tsx index 263ca5eb3..9e0a9a0aa 100644 --- a/vite/src/components/v2/inline-custom-plan-editor/PlanEditorContext.tsx +++ b/vite/src/components/v2/inline-custom-plan-editor/PlanEditorContext.tsx @@ -227,16 +227,12 @@ export function useHasItemChanges() { item1: itemDraft.session.draftItem, item2: itemDraft.session.initialItem, features, - logDifferences: true, }); return !same; } if (!item || !initialItem) { - console.log( - "[useHasItemChanges] no item or initialItem, returning false", - ); return false; } @@ -244,7 +240,6 @@ export function useHasItemChanges() { item1: item, item2: initialItem, features, - logDifferences: true, }); return !same; diff --git a/vite/src/hooks/useSheetScopeEntityId.ts b/vite/src/hooks/useSheetScopeEntityId.ts new file mode 100644 index 000000000..573372414 --- /dev/null +++ b/vite/src/hooks/useSheetScopeEntityId.ts @@ -0,0 +1,34 @@ +import { CusProductStatus, type FullCustomer } from "@autumn/shared"; +import { useMemo, useState } from "react"; + +// If the customer has no active customer-level plan but has at least one +// active entity-level plan, default scope to that entity. Otherwise stay +// customer-scoped. +function pickDefaultScopeEntityId({ + customer, +}: { + customer: FullCustomer | undefined; +}): string | undefined { + const activePlans = + customer?.customer_products?.filter( + (cp) => cp.status === CusProductStatus.Active && !cp.canceled_at, + ) ?? []; + + if (activePlans.some((cp) => !cp.entity_id)) return undefined; + return activePlans.find((cp) => !!cp.entity_id)?.entity_id ?? undefined; +} + +// Sheet-local scope state for Attach / Create Schedule flows. Seeds from the +// `entity_id` URL param if present, otherwise applies the smart default based +// on the customer's existing plans. Changes stay local to the sheet. +export function useSheetScopeEntityId(customer: FullCustomer | undefined) { + const initialFromUrl = useMemo( + () => + new URLSearchParams(window.location.search).get("entity_id") ?? undefined, + [], + ); + + return useState( + initialFromUrl ?? pickDefaultScopeEntityId({ customer }), + ); +} diff --git a/vite/src/views/customers2/components/sheets/AttachProductSheet.tsx b/vite/src/views/customers2/components/sheets/AttachProductSheet.tsx index 6c4c173af..e5474067a 100644 --- a/vite/src/views/customers2/components/sheets/AttachProductSheet.tsx +++ b/vite/src/views/customers2/components/sheets/AttachProductSheet.tsx @@ -35,7 +35,7 @@ import { } from "@/components/v2/sheets/SharedSheetComponents"; import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery"; import { useSheetStore } from "@/hooks/stores/useSheetStore"; -import { useEntity } from "@/hooks/stores/useSubscriptionStore"; +import { useSheetScopeEntityId } from "@/hooks/useSheetScopeEntityId"; import { useEnv } from "@/utils/envUtils"; import { getBackendErr } from "@/utils/genUtils"; import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery"; @@ -205,12 +205,11 @@ function PlanDiffSkeleton() { } function SelectContent() { - const { formValues } = useAttachFormContext(); + const { formValues, entityId, onScopeChange } = useAttachFormContext(); const { closeSheet, setSheet } = useSheetStore(); const itemId = useSheetStore((s) => s.itemId); const hasProductSelected = !!formValues.productId; - const { entityId, setEntityId } = useEntity(); const { customer } = useCusQuery(); const fullCustomer = customer as FullCustomer | null; const entities = fullCustomer?.entities || []; @@ -247,7 +246,9 @@ function SelectContent() { value={entityId ?? CUSTOMER_LEVEL_VALUE} onValueChange={(value) => - setEntityId(value === CUSTOMER_LEVEL_VALUE ? null : value) + onScopeChange?.( + value === CUSTOMER_LEVEL_VALUE ? undefined : value, + ) } options={entityOptions} getOptionValue={getEntityOptionValue} @@ -460,12 +461,14 @@ export function AttachProductSheet() { const { closeSheet } = useSheetStore(); const { customer } = useCusQuery(); const { setIsInlineEditorOpen } = useCustomerContext(); - const { entityId } = useEntity(); + const [scopeEntityId, setScopeEntityId] = useSheetScopeEntityId( + customer as FullCustomer | undefined, + ); return ( setIsInlineEditorOpen(true)} onPlanEditorClose={() => setIsInlineEditorOpen(false)} @@ -474,6 +477,7 @@ export function AttachProductSheet() { toast.success("Checkout URL copied to clipboard"); }} onSuccess={closeSheet} + onScopeChange={setScopeEntityId} > diff --git a/vite/src/views/customers2/components/sheets/CreateScheduleSheet.tsx b/vite/src/views/customers2/components/sheets/CreateScheduleSheet.tsx index 709c511b0..92f73cb9b 100644 --- a/vite/src/views/customers2/components/sheets/CreateScheduleSheet.tsx +++ b/vite/src/views/customers2/components/sheets/CreateScheduleSheet.tsx @@ -6,7 +6,7 @@ import type { } from "@autumn/shared"; import { CusProductStatus, mapToProductItems } from "@autumn/shared"; import { motion } from "motion/react"; -import { useMemo, useRef, useState } from "react"; +import { useMemo, useRef } from "react"; import { toast } from "sonner"; import { AttachFormProvider, @@ -43,6 +43,7 @@ import { import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery"; import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; import { useSheetStore } from "@/hooks/stores/useSheetStore"; +import { useSheetScopeEntityId } from "@/hooks/useSheetScopeEntityId"; import { backendToDisplayQuantity } from "@/utils/billing/prepaidQuantityUtils"; import { useEnv } from "@/utils/envUtils"; import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery"; @@ -142,6 +143,8 @@ export function buildInitialValues({ : { ...EMPTY_SCHEDULE_PLAN }; }), })), + billingBehavior: null, + resetBillingCycle: false, }; } @@ -164,6 +167,8 @@ export function buildInitialValues({ activePlans.length > 0 ? activePlans : [{ ...EMPTY_SCHEDULE_PLAN }], }, ], + billingBehavior: null, + resetBillingCycle: false, }; } @@ -360,15 +365,10 @@ export function getScheduleForScope({ export function CreateScheduleSheet() { const { closeSheet } = useSheetStore(); const { customer, testClockFrozenTimeMs } = useCusQuery({ schedule: true }); - const initialEntityId = - new URLSearchParams(window.location.search).get("entity_id") ?? undefined; - const [scopeEntityId, setScopeEntityId] = useState( - initialEntityId, - ); + const fullCustomer = customer as FullCustomer | undefined; + const [scopeEntityId, setScopeEntityId] = useSheetScopeEntityId(fullCustomer); const { products } = useProductsQuery(); - - const fullCustomer = customer as FullCustomer | undefined; const schedule = getScheduleForScope({ customer: fullCustomer, entityId: scopeEntityId,