Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vite/src/views/products/plan/ProductSheets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export const ProductSheets = () => {
},
}}
>
<EditPlanFeatureSheet />
<EditPlanFeatureSheet key={itemId} />
</ProductItemContext.Provider>
);
case "new-feature":
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,28 @@
import { FeatureType, TierBehavior } from "@autumn/shared";
import {
DropSimpleIcon,
PencilSimpleIcon,
RulerIcon,
} from "@phosphor-icons/react";
import { PencilSimpleIcon } from "@phosphor-icons/react";
import { useState } from "react";
import { IconButton } from "@/components/v2/buttons/IconButton";
import {
useHasItemChanges,
useProduct,
useSheet,
} from "@/components/v2/inline-custom-plan-editor/PlanEditorContext";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/v2/selects/Select";
import { SheetHeader, SheetSection } from "@/components/v2/sheets/InlineSheet";
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
import { getFeature } from "@/utils/product/entitlementUtils";
import { isFeaturePriceItem } from "@/utils/product/getItemType";
import UpdateFeatureSheet from "@/views/products/features/components/UpdateFeatureSheet";
import UpdateCreditSystemSheet from "@/views/products/features/credit-systems/components/UpdateCreditSystemSheet";
import { useProductItemContext } from "@/views/products/product/product-item/ProductItemContext";
import {
cleanTiersForMode,
type VolumePricingMode,
} from "../../utils/tierUtils";
import { AdvancedSettings } from "./AdvancedSettings";
import { BillingType } from "./BillingType";
import { IncludedUsage } from "./IncludedUsage";
import { PricedFeatureSettings } from "./PricedFeatureSettings";
import { PriceSectionTitle } from "./PriceSectionTitle";
import { PriceTiers } from "./PriceTiers";
import { SheetFooterActions } from "./SheetFooterActions";
import { UsageReset } from "./UsageReset";
Expand All @@ -45,6 +39,45 @@ export function EditPlanFeatureSheet({
const hasItemChanges = useHasItemChanges();
const [editFeatureOpen, setEditFeatureOpen] = useState(false);

// Infer initial mode from tier data: if any tier has flat_amount > 0, default to flat
const [volumePricingMode, setVolumePricingMode] = useState<VolumePricingMode>(
() => {
const hasFlatAmount = item?.tiers?.some(
(t) => t.flat_amount != null,
);
return hasFlatAmount ? "flat" : "per_unit";
},
);

const isVolumeBased = item?.tier_behavior === TierBehavior.VolumeBased;
const isMultiTier = (item?.tiers?.length ?? 0) > 1;
const showVolumePricingToggle = isVolumeBased && isMultiTier;

const handleTierBehaviorChange = (val: string) => {
const newBehavior = val as TierBehavior;
const newItem = { ...item, tier_behavior: newBehavior };

// When switching away from volume-based, reset mode and clear flat_amount
if (newBehavior !== TierBehavior.VolumeBased) {
setVolumePricingMode("per_unit");
if (newItem.tiers) {
newItem.tiers = newItem.tiers.map((tier) => ({
...tier,
flat_amount: null,
}));
}
}

setItem(newItem);
};

const handleBeforeCommit = () => {
if (!isVolumeBased) return;
const mode = showVolumePricingToggle ? volumePricingMode : "per_unit";
const cleaned = cleanTiersForMode({ item, mode });
setItem(cleaned);
};

const handleFeatureUpdateSuccess = async (oldId: string, newId: string) => {
if (oldId !== newId && product.items) {
// Wait for features to be refetched to avoid race condition
Expand Down Expand Up @@ -121,61 +154,27 @@ export function EditPlanFeatureSheet({
<SheetSection
title={
item.tiers && item.tiers.length > 1 ? (
<div className="flex items-center justify-between w-full">
<span>Price</span>
<Select
value={item.tier_behavior ?? TierBehavior.Graduated}
onValueChange={(val) =>
setItem({
...item,
tier_behavior: val as TierBehavior,
})
}
>
<SelectTrigger className="w-40 h-6 text-xs" size="sm">
<SelectValue>
{item.tier_behavior === TierBehavior.VolumeBased ? (
<span className="flex items-center gap-2">
<DropSimpleIcon
className="size-3.5"
weight="regular"
/>
Volume-based
</span>
) : (
<span className="flex items-center gap-2">
<RulerIcon
className="size-3.5"
weight="regular"
/>
Graduated
</span>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value={TierBehavior.Graduated}>
<RulerIcon className="size-4" weight="regular" />
Graduated
</SelectItem>
<SelectItem value={TierBehavior.VolumeBased}>
<DropSimpleIcon
className="size-4"
weight="regular"
/>
Volume-based
</SelectItem>
</SelectContent>
</Select>
</div>
<PriceSectionTitle
tierBehavior={
item.tier_behavior ?? TierBehavior.Graduated
}
volumePricingMode={volumePricingMode}
showVolumePricingToggle={showVolumePricingToggle}
onTierBehaviorChange={handleTierBehaviorChange}
onVolumePricingModeChange={setVolumePricingMode}
/>
) : (
"Price"
)
}
className="space-y-3"
>
<div>
<PriceTiers />
<PriceTiers
volumePricingMode={
showVolumePricingToggle ? volumePricingMode : undefined
}
/>
<UsageReset showBillingLabel={true} />
</div>
<PricedFeatureSettings />
Expand All @@ -198,7 +197,10 @@ export function EditPlanFeatureSheet({
</div>

{/* Footer stays at bottom */}
<SheetFooterActions hasChanges={hasChanges} />
<SheetFooterActions
hasChanges={hasChanges}
onBeforeCommit={handleBeforeCommit}
/>

{/* Edit Feature Sheet */}
{feature?.type === FeatureType.CreditSystem ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { TierBehavior } from "@autumn/shared";
import {
CoinsIcon,
DropSimpleIcon,
RulerIcon,
StackIcon,
} from "@phosphor-icons/react";
import { IconCheckbox } from "@/components/v2/checkboxes/IconCheckbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/v2/selects/Select";
import { cn } from "@/lib/utils";
import type { VolumePricingMode } from "../../utils/tierUtils";

export function PriceSectionTitle({
tierBehavior,
volumePricingMode,
showVolumePricingToggle,
onTierBehaviorChange,
onVolumePricingModeChange,
}: {
tierBehavior: TierBehavior;
volumePricingMode: VolumePricingMode;
showVolumePricingToggle: boolean;
onTierBehaviorChange: (val: string) => void;
onVolumePricingModeChange: (mode: VolumePricingMode) => void;
}) {
return (
<div className="flex items-center justify-between w-full">
<span>Price</span>
<div className="flex items-center gap-2">
{showVolumePricingToggle && (
<div className="flex items-center">
<IconCheckbox
icon={<CoinsIcon />}
iconOrientation="left"
variant="secondary"
size="sm"
checked={volumePricingMode === "per_unit"}
onCheckedChange={() => onVolumePricingModeChange("per_unit")}
className={cn(
"rounded-r-none",
volumePricingMode !== "per_unit" && "border-r-0",
)}
>
Per Unit
</IconCheckbox>
<IconCheckbox
icon={<StackIcon />}
iconOrientation="left"
variant="secondary"
size="sm"
checked={volumePricingMode === "flat"}
onCheckedChange={() => onVolumePricingModeChange("flat")}
className={cn(
"rounded-l-none",
volumePricingMode !== "flat" && "border-l-0",
)}
>
Flat Amount
</IconCheckbox>
</div>
)}
<Select value={tierBehavior} onValueChange={onTierBehaviorChange}>
<SelectTrigger className="w-40 h-6 text-xs" size="sm">
<SelectValue>
{tierBehavior === TierBehavior.VolumeBased ? (
<span className="flex items-center gap-2">
<DropSimpleIcon className="size-3.5" weight="regular" />
Volume-based
</span>
) : (
<span className="flex items-center gap-2">
<RulerIcon className="size-3.5" weight="regular" />
Graduated
</span>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value={TierBehavior.Graduated}>
<RulerIcon className="size-4" weight="regular" />
Graduated
</SelectItem>
<SelectItem value={TierBehavior.VolumeBased}>
<DropSimpleIcon className="size-4" weight="regular" />
Volume-based
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
}
Loading
Loading