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
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex">
|
||||
<Switch
|
||||
checked={checked}
|
||||
disabled={!!disabledReason}
|
||||
onCheckedChange={onCheckedChange}
|
||||
/>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
{disabledReason && <TooltipContent>{disabledReason}</TooltipContent>}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<AdvancedSection>
|
||||
<ConfigRow
|
||||
title="Prorate Changes"
|
||||
description="Prorate price differences when changing plans mid-cycle"
|
||||
action={renderToggle({
|
||||
checked: isProrate,
|
||||
onCheckedChange: (checked) =>
|
||||
form.setFieldValue("billingBehavior", checked ? null : "none"),
|
||||
})}
|
||||
/>
|
||||
<ConfigRow
|
||||
title="Reset Billing Cycle"
|
||||
description="Restart the billing cycle from today"
|
||||
action={renderToggle({
|
||||
checked: resetBillingCycle,
|
||||
onCheckedChange: (checked) =>
|
||||
form.setFieldValue("resetBillingCycle", !!checked),
|
||||
})}
|
||||
/>
|
||||
</AdvancedSection>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<CreateScheduleAdvancedSection />
|
||||
<SchedulePreview />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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<CreateScheduleForm>(defaultValues);
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
34
vite/src/hooks/useSheetScopeEntityId.ts
Normal file
34
vite/src/hooks/useSheetScopeEntityId.ts
Normal file
@@ -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<string | undefined>(
|
||||
initialFromUrl ?? pickDefaultScopeEntityId({ customer }),
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
<SearchableSelect<EntityOption>
|
||||
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 (
|
||||
<AttachFormProvider
|
||||
customerId={customer?.id ?? customer?.internal_id ?? ""}
|
||||
entityId={entityId ?? undefined}
|
||||
entityId={scopeEntityId}
|
||||
initialProductId={itemId ?? undefined}
|
||||
onPlanEditorOpen={() => setIsInlineEditorOpen(true)}
|
||||
onPlanEditorClose={() => setIsInlineEditorOpen(false)}
|
||||
@@ -474,6 +477,7 @@ export function AttachProductSheet() {
|
||||
toast.success("Checkout URL copied to clipboard");
|
||||
}}
|
||||
onSuccess={closeSheet}
|
||||
onScopeChange={setScopeEntityId}
|
||||
>
|
||||
<SheetContent />
|
||||
</AttachFormProvider>
|
||||
|
||||
@@ -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<string | undefined>(
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user