feat: upgrade update subscription plan edit
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
interface CompactValueChangeProps {
|
||||
oldValue: string | number | null;
|
||||
newValue: string | number | null;
|
||||
}
|
||||
|
||||
export function CompactValueChange({
|
||||
oldValue,
|
||||
newValue,
|
||||
}: CompactValueChangeProps) {
|
||||
return (
|
||||
<span className="text-xs flex items-center gap-1">
|
||||
<span className="text-red-500">{oldValue}</span>
|
||||
<span className="text-t3">→</span>
|
||||
<span className="text-green-500">{newValue}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -5,114 +5,55 @@ import type {
|
||||
ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import { featureToOptions, UsageModel } from "@autumn/shared";
|
||||
import { InfoIcon, PencilSimpleIcon } from "@phosphor-icons/react";
|
||||
import { PencilSimpleIcon } from "@phosphor-icons/react";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/v2/selects/Select";
|
||||
import { SheetSection } from "@/components/v2/sheets/SharedSheetComponents";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/v2/tooltips/Tooltip";
|
||||
import { BasePriceDisplay } from "@/views/products/plan/components/plan-card/BasePriceDisplay";
|
||||
import { PlanFeatureRow } from "@/views/products/plan/components/plan-card/PlanFeatureRow";
|
||||
import type { UseUpdateSubscriptionForm } from "../hooks/useUpdateSubscriptionForm";
|
||||
import { buildEditsForItem } from "../utils/buildEditsForItem";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
import { SubscriptionItemRow } from "./SubscriptionItemRow";
|
||||
|
||||
interface EditPlanSectionProps {
|
||||
hasCustomizations: boolean;
|
||||
onEditPlan: () => void;
|
||||
product?: ProductV2;
|
||||
originalItems?: ProductItem[];
|
||||
customerProduct?: FullCusProduct;
|
||||
features?: Feature[];
|
||||
form?: UseUpdateSubscriptionForm;
|
||||
numVersions?: number;
|
||||
currentVersion?: number;
|
||||
}
|
||||
|
||||
function SectionTitle({
|
||||
hasCustomizations,
|
||||
form,
|
||||
numVersions,
|
||||
currentVersion,
|
||||
}: {
|
||||
hasCustomizations: boolean;
|
||||
form?: UseUpdateSubscriptionForm;
|
||||
numVersions?: number;
|
||||
currentVersion?: number;
|
||||
}) {
|
||||
const showVersionSelector =
|
||||
form && numVersions !== undefined && numVersions > 1;
|
||||
|
||||
const versionOptions = showVersionSelector
|
||||
? Array.from(
|
||||
{ length: numVersions },
|
||||
(_, index) => numVersions - index,
|
||||
).map((version) => ({
|
||||
label: `Version ${version}`,
|
||||
value: String(version),
|
||||
}))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<span className="flex items-center justify-between w-full gap-2">
|
||||
<span className="flex items-center gap-1.5">
|
||||
Plan Configuration
|
||||
{hasCustomizations && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon
|
||||
size={14}
|
||||
weight="fill"
|
||||
className="text-amber-500 cursor-help"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
This subscription's configuration was edited. See changes below.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
{showVersionSelector && (
|
||||
<form.AppField name="version">
|
||||
{(field) => (
|
||||
<Select
|
||||
value={String(field.state.value ?? currentVersion)}
|
||||
onValueChange={(value) => field.handleChange(Number(value))}
|
||||
>
|
||||
<SelectTrigger className="w-fit h-7 text-xs whitespace-nowrap">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{versionOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</form.AppField>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
prepaidOptions?: Record<string, number>;
|
||||
initialPrepaidOptions?: Record<string, number>;
|
||||
}
|
||||
|
||||
export function EditPlanSection({
|
||||
hasCustomizations,
|
||||
onEditPlan,
|
||||
product,
|
||||
originalItems,
|
||||
customerProduct,
|
||||
features,
|
||||
form,
|
||||
numVersions,
|
||||
currentVersion,
|
||||
prepaidOptions = {},
|
||||
initialPrepaidOptions = {},
|
||||
}: EditPlanSectionProps) {
|
||||
const originalItemsMap = new Map(
|
||||
originalItems?.filter((i) => i.feature_id).map((i) => [i.feature_id, i]) ??
|
||||
[],
|
||||
);
|
||||
|
||||
const currentFeatureIds = new Set(
|
||||
product?.items?.map((i) => i.feature_id).filter(Boolean) ?? [],
|
||||
);
|
||||
const deletedItems =
|
||||
originalItems?.filter(
|
||||
(i) => i.feature_id && !currentFeatureIds.has(i.feature_id),
|
||||
) ?? [];
|
||||
|
||||
return (
|
||||
<SheetSection
|
||||
title={
|
||||
@@ -125,39 +66,64 @@ export function EditPlanSection({
|
||||
}
|
||||
withSeparator
|
||||
>
|
||||
{product?.items && product.items.length > 0 && (
|
||||
{(product?.items?.length ?? 0) > 0 || deletedItems.length > 0 ? (
|
||||
<>
|
||||
<div className="flex gap-2 justify-between items-center h-6 mb-3">
|
||||
<BasePriceDisplay product={product} readOnly={true} />
|
||||
</div>
|
||||
<div className="space-y-2 mb-4">
|
||||
{product.items.map((item: ProductItem, index: number) => {
|
||||
{product?.items?.map((item: ProductItem, index: number) => {
|
||||
if (!item.feature_id) return null;
|
||||
|
||||
const feature = features?.find((f) => f.id === item.feature_id);
|
||||
const featureId = item.feature_id;
|
||||
const featureForOptions = features?.find(
|
||||
(f) => f.id === featureId,
|
||||
);
|
||||
const prepaidOption = featureToOptions({
|
||||
feature,
|
||||
feature: featureForOptions,
|
||||
options: customerProduct?.options,
|
||||
});
|
||||
|
||||
const prepaidQuantity =
|
||||
item.usage_model === UsageModel.Prepaid
|
||||
? prepaidOption?.quantity
|
||||
: null;
|
||||
const isPrepaid = item.usage_model === UsageModel.Prepaid;
|
||||
const currentPrepaidQuantity = isPrepaid
|
||||
? prepaidOptions[featureId]
|
||||
: prepaidOption?.quantity;
|
||||
const initialPrepaidQuantity = isPrepaid
|
||||
? initialPrepaidOptions[featureId]
|
||||
: undefined;
|
||||
|
||||
const originalItem = originalItemsMap.get(featureId);
|
||||
const isCreated =
|
||||
!originalItem && originalItems && originalItems.length > 0;
|
||||
const edits = buildEditsForItem({
|
||||
item,
|
||||
originalItem,
|
||||
prepaidQuantity: currentPrepaidQuantity,
|
||||
initialPrepaidQuantity,
|
||||
});
|
||||
|
||||
return (
|
||||
<PlanFeatureRow
|
||||
key={item.feature_id || item.price_id || index}
|
||||
<SubscriptionItemRow
|
||||
key={featureId || item.price_id || index}
|
||||
item={item}
|
||||
index={index}
|
||||
readOnly={true}
|
||||
prepaidQuantity={prepaidQuantity}
|
||||
edits={edits}
|
||||
prepaidQuantity={currentPrepaidQuantity}
|
||||
form={form}
|
||||
featureId={featureId}
|
||||
isCreated={isCreated}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{deletedItems.map((item: ProductItem, index: number) => (
|
||||
<SubscriptionItemRow
|
||||
key={`deleted-${item.feature_id || index}`}
|
||||
item={item}
|
||||
isDeleted
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
<Button variant="secondary" onClick={onEditPlan} className="w-full">
|
||||
<PencilSimpleIcon size={14} className="mr-1" />
|
||||
Edit Plan Items
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { InfoIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/v2/selects/Select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/v2/tooltips/Tooltip";
|
||||
import type { UseUpdateSubscriptionForm } from "../hooks/useUpdateSubscriptionForm";
|
||||
|
||||
interface SectionTitleProps {
|
||||
hasCustomizations: boolean;
|
||||
form?: UseUpdateSubscriptionForm;
|
||||
numVersions?: number;
|
||||
currentVersion?: number;
|
||||
}
|
||||
|
||||
export function SectionTitle({
|
||||
hasCustomizations,
|
||||
form,
|
||||
numVersions,
|
||||
currentVersion,
|
||||
}: SectionTitleProps) {
|
||||
const showVersionSelector =
|
||||
form && numVersions !== undefined && numVersions > 1;
|
||||
|
||||
const versionOptions = showVersionSelector
|
||||
? Array.from(
|
||||
{ length: numVersions },
|
||||
(_, index) => numVersions - index,
|
||||
).map((version) => ({
|
||||
label: `Version ${version}`,
|
||||
value: String(version),
|
||||
}))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<span className="flex items-center justify-between w-full gap-2">
|
||||
<span className="flex items-center gap-1.5">
|
||||
Plan Configuration
|
||||
{hasCustomizations && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon
|
||||
size={14}
|
||||
weight="fill"
|
||||
className="text-amber-500 cursor-help"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
This subscription's configuration was edited. See changes below.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
{showVersionSelector && (
|
||||
<form.AppField name="version">
|
||||
{(field) => (
|
||||
<Select
|
||||
value={String(field.state.value ?? currentVersion)}
|
||||
onValueChange={(value) => field.handleChange(Number(value))}
|
||||
>
|
||||
<SelectTrigger className="w-fit h-7 text-xs whitespace-nowrap">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{versionOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</form.AppField>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type StatusBadgeVariant = "created" | "removed";
|
||||
|
||||
const variantStyles: Record<StatusBadgeVariant, string> = {
|
||||
created: "bg-green-500/20 text-green-500",
|
||||
removed: "bg-red-500/20 text-red-500",
|
||||
};
|
||||
|
||||
interface StatusBadgeProps {
|
||||
variant: StatusBadgeVariant;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function StatusBadge({ variant, children }: StatusBadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"px-2 py-0.5 rounded-md text-xs font-medium",
|
||||
variantStyles[variant],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import {
|
||||
getProductItemDisplay,
|
||||
type ProductItem,
|
||||
UsageModel,
|
||||
} from "@autumn/shared";
|
||||
import {
|
||||
CaretDownIcon,
|
||||
CheckIcon,
|
||||
PencilSimpleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useState } from "react";
|
||||
import { IconButton } from "@/components/v2/buttons/IconButton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/v2/tooltips/Tooltip";
|
||||
import { useOrg } from "@/hooks/common/useOrg";
|
||||
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PlanFeatureIcon } from "@/views/products/plan/components/plan-card/PlanFeatureIcon";
|
||||
import { CustomDotIcon } from "@/views/products/plan/components/plan-card/PlanFeatureRow";
|
||||
import type { UseUpdateSubscriptionForm } from "../hooks/useUpdateSubscriptionForm";
|
||||
import type { ItemEdit } from "../types/summary";
|
||||
import { getEditIcon } from "../utils/getEditIcon";
|
||||
import { CompactValueChange } from "./CompactValueChange";
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
|
||||
interface SubscriptionItemRowProps {
|
||||
item: ProductItem;
|
||||
edits?: ItemEdit[];
|
||||
form?: UseUpdateSubscriptionForm;
|
||||
featureId?: string;
|
||||
prepaidQuantity?: number | null;
|
||||
isDeleted?: boolean;
|
||||
isCreated?: boolean;
|
||||
}
|
||||
|
||||
function EditRow({
|
||||
edit,
|
||||
showRing = true,
|
||||
}: {
|
||||
edit: ItemEdit;
|
||||
showRing?: boolean;
|
||||
}) {
|
||||
const ringClass = edit.isUpgrade
|
||||
? "ring-1 ring-inset ring-green-500/50"
|
||||
: "ring-1 ring-inset ring-red-500/50";
|
||||
|
||||
const renderDescription = () => {
|
||||
const match = edit.description.match(
|
||||
/^(.+\bfrom\s+)(\S+)(\s+to\s+)(\S+)(.*)$/,
|
||||
);
|
||||
if (match) {
|
||||
const [, prefix, oldVal, middle, newVal, suffix] = match;
|
||||
return (
|
||||
<span className="text-xs text-t3">
|
||||
{prefix}
|
||||
<span className="text-red-500 font-medium">{oldVal}</span>
|
||||
{middle}
|
||||
<span className="text-green-500 font-medium">{newVal}</span>
|
||||
<span className="text-t4">{suffix}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span className="text-xs text-t3">{edit.description}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full h-9 px-3 rounded-xl",
|
||||
showRing && "input-base",
|
||||
showRing && ringClass,
|
||||
)}
|
||||
>
|
||||
{showRing && getEditIcon(edit.icon, edit.isUpgrade)}
|
||||
{showRing ? (
|
||||
renderDescription()
|
||||
) : (
|
||||
<CompactValueChange oldValue={edit.oldValue} newValue={edit.newValue} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SubscriptionItemRow({
|
||||
item,
|
||||
edits = [],
|
||||
form,
|
||||
featureId,
|
||||
prepaidQuantity,
|
||||
isDeleted = false,
|
||||
isCreated = false,
|
||||
}: SubscriptionItemRowProps) {
|
||||
const { org } = useOrg();
|
||||
const { features } = useFeaturesQuery();
|
||||
const [isAccordionOpen, setIsAccordionOpen] = useState(false);
|
||||
const [isEditingQuantity, setIsEditingQuantity] = useState(false);
|
||||
|
||||
const display = getProductItemDisplay({
|
||||
item,
|
||||
features,
|
||||
currency: org?.default_currency || "USD",
|
||||
fullDisplay: true,
|
||||
amountFormatOptions: { currencyDisplay: "narrowSymbol" },
|
||||
});
|
||||
|
||||
const feature = features.find((f) => f.id === item.feature_id);
|
||||
const hasFeatureName = feature?.name && feature.name.trim() !== "";
|
||||
const displayText = hasFeatureName
|
||||
? display.primary_text
|
||||
: "Name your feature";
|
||||
const isPrepaid = item.usage_model === UsageModel.Prepaid;
|
||||
|
||||
const hasMultipleEdits = edits.length > 1;
|
||||
const singleEdit = edits.length === 1 ? edits[0] : null;
|
||||
const hasEditableEdit = edits.some((e) => e.editable);
|
||||
|
||||
const getRowRingClass = () => {
|
||||
if (isDeleted) return "ring-1 ring-inset ring-red-500/50 opacity-60";
|
||||
if (isCreated) return "ring-1 ring-inset ring-green-500/50";
|
||||
if (edits.length === 0) return "";
|
||||
return "ring-1 ring-inset ring-amber-500/50";
|
||||
};
|
||||
|
||||
const renderRowIndicator = () => {
|
||||
if (isDeleted) return <StatusBadge variant="removed">Removed</StatusBadge>;
|
||||
if (isCreated) return <StatusBadge variant="created">Created</StatusBadge>;
|
||||
|
||||
if (hasMultipleEdits) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 text-xs text-t3 hover:text-t1"
|
||||
>
|
||||
{edits.length} Changes
|
||||
<CaretDownIcon
|
||||
size={14}
|
||||
className={cn(
|
||||
"transition-transform duration-200",
|
||||
isAccordionOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (singleEdit?.editable) {
|
||||
return <EditRow edit={singleEdit} showRing={false} />;
|
||||
}
|
||||
|
||||
if (singleEdit) {
|
||||
return (
|
||||
<CompactValueChange
|
||||
oldValue={singleEdit.oldValue}
|
||||
newValue={singleEdit.newValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPrepaid && prepaidQuantity && edits.length === 0) {
|
||||
return (
|
||||
<span className="bg-muted px-1.5 py-0.5 rounded-md text-xs">
|
||||
x{parseFloat(Number(prepaidQuantity).toFixed(2))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const showPrepaidOutside =
|
||||
isPrepaid && form && featureId && edits.length === 0;
|
||||
|
||||
const handleRowClick = () => {
|
||||
if (hasMultipleEdits) setIsAccordionOpen(!isAccordionOpen);
|
||||
};
|
||||
|
||||
const handleRowKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (hasMultipleEdits && (e.key === "Enter" || e.key === " ")) {
|
||||
e.preventDefault();
|
||||
setIsAccordionOpen(!isAccordionOpen);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
role={hasMultipleEdits ? "button" : undefined}
|
||||
tabIndex={hasMultipleEdits ? 0 : undefined}
|
||||
className={cn(
|
||||
"flex items-center flex-1 min-w-0 h-10 px-3 rounded-xl input-base",
|
||||
getRowRingClass(),
|
||||
hasMultipleEdits && "cursor-pointer",
|
||||
)}
|
||||
onClick={handleRowClick}
|
||||
onKeyDown={handleRowKeyDown}
|
||||
>
|
||||
<div className="flex flex-row items-center flex-1 gap-2 min-w-0 overflow-hidden">
|
||||
<div className="flex flex-row items-center gap-1 shrink-0">
|
||||
<PlanFeatureIcon item={item} position="left" />
|
||||
<CustomDotIcon />
|
||||
<PlanFeatureIcon item={item} position="right" />
|
||||
</div>
|
||||
<p className="whitespace-nowrap truncate flex-1 min-w-0">
|
||||
<span className={cn("text-body", !hasFeatureName && "text-t4!")}>
|
||||
{displayText}
|
||||
</span>
|
||||
<span className="text-body-secondary">
|
||||
{" "}
|
||||
{display.secondary_text}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{renderRowIndicator()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isDeleted &&
|
||||
(showPrepaidOutside || hasEditableEdit) &&
|
||||
form &&
|
||||
featureId && (
|
||||
<div className="flex items-center h-10 px-3 rounded-xl input-base w-fit shrink-0 gap-2">
|
||||
{isEditingQuantity ? (
|
||||
<>
|
||||
<form.AppField name={`prepaidOptions.${featureId}`}>
|
||||
{(field) => (
|
||||
<field.QuantityField label="" min={0} hideFieldInfo />
|
||||
)}
|
||||
</form.AppField>
|
||||
<IconButton
|
||||
icon={<CheckIcon size={14} />}
|
||||
variant="skeleton"
|
||||
size="sm"
|
||||
className="text-t4 hover:text-t2 hover:bg-muted"
|
||||
onClick={() => setIsEditingQuantity(false)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-sm tabular-nums text-t3">
|
||||
x{prepaidQuantity}
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<IconButton
|
||||
icon={<PencilSimpleIcon size={14} />}
|
||||
variant="skeleton"
|
||||
size="sm"
|
||||
className="text-t4 hover:text-t2 hover:bg-muted"
|
||||
onClick={() => setIsEditingQuantity(true)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Update prepaid quantity</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isDeleted && hasMultipleEdits && (
|
||||
<div
|
||||
className={cn(
|
||||
"grid transition-[grid-template-rows] duration-200 ease-out",
|
||||
isAccordionOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
|
||||
)}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="flex flex-col gap-2 pt-2 pl-8">
|
||||
{edits.map((edit) => (
|
||||
<EditRow key={edit.id} edit={edit} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PreviewUpdateSubscriptionResponse } from "@autumn/shared";
|
||||
import { format } from "date-fns";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { LineItemsPreview } from "@/components/v2/LineItemsPreview";
|
||||
import { SheetSection } from "@/components/v2/sheets/SharedSheetComponents";
|
||||
@@ -32,6 +33,9 @@ export function UpdateSubscriptionPreviewSection({
|
||||
label: "Next Cycle",
|
||||
amount: previewData.next_cycle.total,
|
||||
variant: "secondary" as const,
|
||||
badge: previewData.next_cycle.starts_at
|
||||
? format(new Date(previewData.next_cycle.starts_at), "MMM d, yyyy")
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,27 @@
|
||||
import type { FullCusProduct, ProductItem } from "@autumn/shared";
|
||||
import type { FullCusProduct } from "@autumn/shared";
|
||||
import { useStore } from "@tanstack/react-form";
|
||||
import { useMemo } from "react";
|
||||
import { SheetSection } from "@/components/v2/sheets/SharedSheetComponents";
|
||||
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
|
||||
import type { PrepaidItemWithFeature } from "@/hooks/stores/useProductStore";
|
||||
import type { UseUpdateSubscriptionForm } from "../hooks/useUpdateSubscriptionForm";
|
||||
import type { SummaryItem } from "../types/summary";
|
||||
import { generateItemChanges } from "../utils/generateItemChanges";
|
||||
import { generatePrepaidChanges } from "../utils/generatePrepaidChanges";
|
||||
import { generateTrialChanges } from "../utils/generateTrialChanges";
|
||||
import { generateVersionChanges } from "../utils/generateVersionChanges";
|
||||
import { SummaryItemRow } from "./SummaryItemRow";
|
||||
|
||||
interface UpdateSubscriptionSummaryProps {
|
||||
form: UseUpdateSubscriptionForm;
|
||||
prepaidItems: PrepaidItemWithFeature[];
|
||||
customerProduct: FullCusProduct;
|
||||
currentVersion: number;
|
||||
currency?: string;
|
||||
originalItems?: ProductItem[];
|
||||
}
|
||||
|
||||
export function UpdateSubscriptionSummary({
|
||||
form,
|
||||
prepaidItems,
|
||||
customerProduct,
|
||||
currentVersion,
|
||||
currency = "usd",
|
||||
originalItems,
|
||||
}: UpdateSubscriptionSummaryProps) {
|
||||
const formValues = useStore(form.store, (state) => state.values);
|
||||
const { features } = useFeaturesQuery();
|
||||
|
||||
const defaultValues = form.options.defaultValues;
|
||||
const initialPrepaidOptions = defaultValues?.prepaidOptions ?? {};
|
||||
|
||||
const changes = useMemo((): SummaryItem[] => {
|
||||
const trialChanges = generateTrialChanges({
|
||||
@@ -48,52 +36,14 @@ export function UpdateSubscriptionSummary({
|
||||
selectedVersion: formValues.version,
|
||||
});
|
||||
|
||||
const itemChanges = generateItemChanges({
|
||||
originalItems,
|
||||
customizedItems: formValues.items,
|
||||
features,
|
||||
prepaidOptions: formValues.prepaidOptions,
|
||||
});
|
||||
|
||||
// Get set of newly added feature IDs to filter out their prepaid changes
|
||||
// (prepaid info is already included in the item addition description)
|
||||
const newlyAddedFeatureIds = new Set(
|
||||
itemChanges
|
||||
.filter((change) => change.id.startsWith("item-added-"))
|
||||
.map((change) => change.id.replace("item-added-", "")),
|
||||
);
|
||||
|
||||
const prepaidChanges = generatePrepaidChanges({
|
||||
prepaidItems,
|
||||
currentOptions: formValues.prepaidOptions,
|
||||
initialOptions: initialPrepaidOptions,
|
||||
currency,
|
||||
}).filter((change) => {
|
||||
// Filter out prepaid changes for newly added items
|
||||
const featureId = change.id.replace("prepaid-", "");
|
||||
return !newlyAddedFeatureIds.has(featureId);
|
||||
});
|
||||
|
||||
return [
|
||||
...versionChanges,
|
||||
...itemChanges,
|
||||
...prepaidChanges,
|
||||
...trialChanges,
|
||||
];
|
||||
return [...versionChanges, ...trialChanges];
|
||||
}, [
|
||||
prepaidItems,
|
||||
formValues.prepaidOptions,
|
||||
formValues.removeTrial,
|
||||
formValues.trialLength,
|
||||
formValues.trialDuration,
|
||||
formValues.version,
|
||||
formValues.items,
|
||||
initialPrepaidOptions,
|
||||
customerProduct,
|
||||
currentVersion,
|
||||
currency,
|
||||
features,
|
||||
originalItems,
|
||||
]);
|
||||
|
||||
if (changes.length === 0) return null;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// Components
|
||||
export * from "./components/EditPlanSection";
|
||||
export * from "./components/FreeTrialSection";
|
||||
export * from "./components/PrepaidQuantitySection";
|
||||
export * from "./components/PreviewErrorDisplay";
|
||||
export * from "./components/SubscriptionItemRow";
|
||||
export * from "./components/UpdateSubscriptionFooter";
|
||||
export * from "./components/UpdateSubscriptionPreviewSection";
|
||||
export * from "./components/UpdateSubscriptionSummary";
|
||||
|
||||
@@ -11,3 +11,21 @@ export interface SummaryItem {
|
||||
/** The product item for rendering icons (prepaid and item changes) */
|
||||
productItem?: ProductItem;
|
||||
}
|
||||
|
||||
export type EditIconType = "price" | "tier" | "usage" | "units" | "prepaid";
|
||||
|
||||
export interface ItemEdit {
|
||||
id: string;
|
||||
type: "config" | "prepaid";
|
||||
label: string;
|
||||
/** Icon type for the edit */
|
||||
icon: EditIconType;
|
||||
/** Full sentence description for accordion display */
|
||||
description: string;
|
||||
oldValue: string | number | null;
|
||||
newValue: string | number | null;
|
||||
/** Whether this is an upgrade (true) or downgrade (false) */
|
||||
isUpgrade: boolean;
|
||||
/** Whether this edit has an inline editor (prepaid quantity) */
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { type ProductItem, UsageModel } from "@autumn/shared";
|
||||
import type { ItemEdit } from "../types/summary";
|
||||
|
||||
/** Format tier threshold value for display */
|
||||
function formatTierValue(value: string | number | "inf"): string {
|
||||
return value === "inf" ? "Infinite" : String(value);
|
||||
}
|
||||
|
||||
export function buildEditsForItem({
|
||||
item,
|
||||
originalItem,
|
||||
prepaidQuantity,
|
||||
initialPrepaidQuantity,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
originalItem?: ProductItem;
|
||||
prepaidQuantity?: number;
|
||||
initialPrepaidQuantity?: number;
|
||||
}): ItemEdit[] {
|
||||
const edits: ItemEdit[] = [];
|
||||
const isPrepaid = item.usage_model === UsageModel.Prepaid;
|
||||
|
||||
if (originalItem) {
|
||||
if (
|
||||
originalItem.price !== item.price &&
|
||||
originalItem.price !== null &&
|
||||
originalItem.price !== undefined &&
|
||||
item.price !== null &&
|
||||
item.price !== undefined
|
||||
) {
|
||||
const oldPrice = originalItem.price;
|
||||
const newPrice = item.price;
|
||||
const isUpgrade = newPrice < oldPrice;
|
||||
edits.push({
|
||||
id: `price-${item.feature_id}`,
|
||||
type: "config",
|
||||
icon: "price",
|
||||
label: "Price",
|
||||
description: isUpgrade
|
||||
? `Price decreased from $${oldPrice} to $${newPrice}`
|
||||
: `Price increased from $${oldPrice} to $${newPrice}`,
|
||||
oldValue: `$${oldPrice}`,
|
||||
newValue: `$${newPrice}`,
|
||||
isUpgrade,
|
||||
});
|
||||
}
|
||||
|
||||
const oldTiers = originalItem.tiers ?? [];
|
||||
const newTiers = item.tiers ?? [];
|
||||
|
||||
for (let i = 0; i < newTiers.length; i++) {
|
||||
const newTier = newTiers[i];
|
||||
const oldTier = oldTiers[i];
|
||||
const tierLabel = `${formatTierValue(newTier.to)} units`;
|
||||
|
||||
if (!oldTier) {
|
||||
const prevTierTo = i === 0 ? "0" : (oldTiers[i - 1]?.to ?? "0");
|
||||
const prevLabel = formatTierValue(prevTierTo);
|
||||
edits.push({
|
||||
id: `tier-${item.feature_id}-${i}`,
|
||||
type: "config",
|
||||
icon: "tier",
|
||||
label: "Pricing Tier",
|
||||
description: `Added tier: after ${prevLabel} units costs $${newTier.amount}`,
|
||||
oldValue: null,
|
||||
newValue: `$${newTier.amount}`,
|
||||
isUpgrade: true,
|
||||
});
|
||||
} else if (oldTier.amount !== newTier.amount) {
|
||||
const isUpgrade = newTier.amount < oldTier.amount;
|
||||
edits.push({
|
||||
id: `tier-${item.feature_id}-${i}`,
|
||||
type: "config",
|
||||
icon: "tier",
|
||||
label: "Pricing Tier",
|
||||
description: isUpgrade
|
||||
? `Tier price decreased from $${oldTier.amount} to $${newTier.amount} (up to ${tierLabel})`
|
||||
: `Tier price increased from $${oldTier.amount} to $${newTier.amount} (up to ${tierLabel})`,
|
||||
oldValue: `$${oldTier.amount}`,
|
||||
newValue: `$${newTier.amount}`,
|
||||
isUpgrade,
|
||||
});
|
||||
} else if (oldTier.to !== newTier.to) {
|
||||
const oldLabel = formatTierValue(oldTier.to);
|
||||
const newLabel = formatTierValue(newTier.to);
|
||||
const isUpgrade =
|
||||
newTier.to === "inf" ||
|
||||
(oldTier.to !== "inf" && Number(newTier.to) > Number(oldTier.to));
|
||||
edits.push({
|
||||
id: `tier-${item.feature_id}-${i}-threshold`,
|
||||
type: "config",
|
||||
icon: "tier",
|
||||
label: "Pricing Tier",
|
||||
description: isUpgrade
|
||||
? `Tier threshold increased from ${oldLabel} to ${newLabel} units`
|
||||
: `Tier threshold decreased from ${oldLabel} to ${newLabel} units`,
|
||||
oldValue: oldLabel,
|
||||
newValue: newLabel,
|
||||
isUpgrade,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = newTiers.length; i < oldTiers.length; i++) {
|
||||
const oldTier = oldTiers[i];
|
||||
const tierLabel = formatTierValue(oldTier.to);
|
||||
edits.push({
|
||||
id: `tier-${item.feature_id}-${i}-removed`,
|
||||
type: "config",
|
||||
icon: "tier",
|
||||
label: "Pricing Tier",
|
||||
description: `Removed tier: up to ${tierLabel} units at $${oldTier.amount}`,
|
||||
oldValue: `$${oldTier.amount}`,
|
||||
newValue: null,
|
||||
isUpgrade: true,
|
||||
});
|
||||
}
|
||||
|
||||
const oldUsage = originalItem.included_usage ?? 0;
|
||||
const newUsage = item.included_usage ?? 0;
|
||||
if (oldUsage !== newUsage) {
|
||||
const formatUsageValue = (val: string | number) =>
|
||||
val === "inf" ? "unlimited" : String(val);
|
||||
const formatUsageDisplay = (val: string | number) =>
|
||||
val === "inf" ? "Unlimited" : `${val} Included`;
|
||||
const oldNum =
|
||||
oldUsage === "inf" ? Number.POSITIVE_INFINITY : Number(oldUsage);
|
||||
const newNum =
|
||||
newUsage === "inf" ? Number.POSITIVE_INFINITY : Number(newUsage);
|
||||
const isUpgrade = newNum > oldNum;
|
||||
edits.push({
|
||||
id: `usage-${item.feature_id}`,
|
||||
type: "config",
|
||||
icon: "usage",
|
||||
label: "Included Usage",
|
||||
description: isUpgrade
|
||||
? `Included usage increased from ${formatUsageValue(oldUsage)} to ${formatUsageValue(newUsage)}`
|
||||
: `Included usage decreased from ${formatUsageValue(oldUsage)} to ${formatUsageValue(newUsage)}`,
|
||||
oldValue: formatUsageDisplay(oldUsage),
|
||||
newValue: formatUsageDisplay(newUsage),
|
||||
isUpgrade,
|
||||
});
|
||||
}
|
||||
|
||||
const oldUnits = originalItem.billing_units ?? 1;
|
||||
const newUnits = item.billing_units ?? 1;
|
||||
if (oldUnits !== newUnits) {
|
||||
const isUpgrade = newUnits > oldUnits;
|
||||
edits.push({
|
||||
id: `units-${item.feature_id}`,
|
||||
type: "config",
|
||||
icon: "units",
|
||||
label: "Billing Units",
|
||||
description: isUpgrade
|
||||
? `Billing units increased from ${oldUnits} to ${newUnits}`
|
||||
: `Billing units decreased from ${oldUnits} to ${newUnits}`,
|
||||
oldValue: `${oldUnits} units`,
|
||||
newValue: `${newUnits} units`,
|
||||
isUpgrade,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isPrepaid &&
|
||||
initialPrepaidQuantity !== undefined &&
|
||||
prepaidQuantity !== undefined &&
|
||||
prepaidQuantity !== initialPrepaidQuantity
|
||||
) {
|
||||
const isUpgrade = prepaidQuantity > initialPrepaidQuantity;
|
||||
edits.push({
|
||||
id: `prepaid-${item.feature_id}`,
|
||||
type: "prepaid",
|
||||
icon: "prepaid",
|
||||
label: "Prepaid Quantity",
|
||||
description: isUpgrade
|
||||
? `Prepaid quantity increased from ${initialPrepaidQuantity} to ${prepaidQuantity}`
|
||||
: `Prepaid quantity decreased from ${initialPrepaidQuantity} to ${prepaidQuantity}`,
|
||||
oldValue: `${initialPrepaidQuantity} Prepaid`,
|
||||
newValue: `${prepaidQuantity} Prepaid`,
|
||||
isUpgrade,
|
||||
editable: true,
|
||||
});
|
||||
}
|
||||
|
||||
return edits;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
CurrencyDollarIcon,
|
||||
HashIcon,
|
||||
PackageIcon,
|
||||
StackIcon,
|
||||
TagIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { EditIconType } from "../types/summary";
|
||||
|
||||
export function getEditIcon(iconType: EditIconType, isUpgrade: boolean) {
|
||||
const iconProps = {
|
||||
size: 14,
|
||||
className: cn("shrink-0", isUpgrade ? "text-green-500" : "text-red-500"),
|
||||
};
|
||||
switch (iconType) {
|
||||
case "price":
|
||||
return <CurrencyDollarIcon {...iconProps} />;
|
||||
case "tier":
|
||||
return <StackIcon {...iconProps} />;
|
||||
case "usage":
|
||||
return <HashIcon {...iconProps} />;
|
||||
case "units":
|
||||
return <TagIcon {...iconProps} />;
|
||||
case "prepaid":
|
||||
return <PackageIcon {...iconProps} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import { Input } from "@/components/v2/inputs/Input";
|
||||
import { useFieldContext } from "@/hooks/form/form-context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import SmallSpinner from "../../SmallSpinner";
|
||||
|
||||
export function QuantityField({
|
||||
@@ -14,6 +15,7 @@ export function QuantityField({
|
||||
max,
|
||||
className,
|
||||
hideFieldInfo,
|
||||
compact,
|
||||
}: {
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
@@ -22,6 +24,7 @@ export function QuantityField({
|
||||
max?: number;
|
||||
className?: string;
|
||||
hideFieldInfo?: boolean;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const field = useFieldContext<number>();
|
||||
|
||||
@@ -66,11 +69,18 @@ export function QuantityField({
|
||||
<div className={className}>
|
||||
{label && <Label>{label}</Label>}
|
||||
<div className="relative flex items-center">
|
||||
<div className="inline-flex rounded-lg overflow-hidden border border-border w-fit h-6 items-center">
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex rounded-lg overflow-hidden border border-border w-fit h-6 items-center",
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
aria-label="Decrease quantity"
|
||||
className="disabled:pointer-events-none disabled:opacity-50 rounded-none border-none h-input px-3"
|
||||
className={cn(
|
||||
"disabled:pointer-events-none disabled:opacity-50 rounded-none border-none h-input",
|
||||
compact ? "px-2" : "px-3",
|
||||
)}
|
||||
disabled={field.state.value <= min}
|
||||
onClick={handleDecrement}
|
||||
size="sm"
|
||||
@@ -82,9 +92,11 @@ export function QuantityField({
|
||||
<div className="relative border-x border-border">
|
||||
<Input
|
||||
variant="headless"
|
||||
className="text-sm text-center w-16 h-input p-2"
|
||||
className={cn(
|
||||
"text-sm text-center h-input p-2",
|
||||
compact ? "w-10" : "w-16",
|
||||
)}
|
||||
onChange={handleInputChange}
|
||||
// placeholder={placeholder}
|
||||
type="number"
|
||||
value={field.state.value ?? ""}
|
||||
min={min}
|
||||
@@ -100,7 +112,10 @@ export function QuantityField({
|
||||
<Button
|
||||
type="button"
|
||||
aria-label="Increase quantity"
|
||||
className="disabled:pointer-events-none disabled:opacity-50 rounded-none border-none h-input px-3"
|
||||
className={cn(
|
||||
"disabled:pointer-events-none disabled:opacity-50 rounded-none border-none h-input",
|
||||
compact ? "px-2" : "px-3",
|
||||
)}
|
||||
disabled={max !== undefined && field.state.value >= max}
|
||||
onClick={handleIncrement}
|
||||
size="sm"
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Badge } from "@/components/v2/badges/badge";
|
||||
import { LoadingShimmerText } from "@/components/v2/LoadingShimmerText";
|
||||
import { SheetSection } from "@/components/v2/sheets/SharedSheetComponents";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -33,6 +34,7 @@ export interface LineItemsPreviewProps<T extends BillingLineItem> {
|
||||
label: string;
|
||||
amount: number;
|
||||
variant?: "primary" | "secondary";
|
||||
badge?: string;
|
||||
}[];
|
||||
/** Accordion title for line items */
|
||||
accordionTitle?: string;
|
||||
@@ -81,9 +83,9 @@ export function LineItemsPreview<T extends BillingLineItem>({
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-2 pt-1">
|
||||
<div className="space-y-2">
|
||||
{filteredItems.map((item, index) => (
|
||||
{filteredItems.map((item) => (
|
||||
<div
|
||||
key={index}
|
||||
key={item.description}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<span className="text-sm truncate max-w-75 text-t3">
|
||||
@@ -112,16 +114,21 @@ export function LineItemsPreview<T extends BillingLineItem>({
|
||||
{/* Totals */}
|
||||
{totals.length > 0 && (
|
||||
<div className="space-y-1 text-sm">
|
||||
{totals.map((total, index) => (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
{totals.map((total) => (
|
||||
<div
|
||||
key={total.label}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<span
|
||||
className={
|
||||
className={cn(
|
||||
"font-medium flex items-center gap-2",
|
||||
total.variant === "secondary"
|
||||
? "font-medium text-t4"
|
||||
: "font-medium text-foreground"
|
||||
}
|
||||
? "text-t4"
|
||||
: "text-foreground",
|
||||
)}
|
||||
>
|
||||
{total.label}
|
||||
{total.badge && <Badge variant="muted">{total.badge}</Badge>}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import {
|
||||
type FrontendProduct,
|
||||
type FullCusProduct,
|
||||
getProductItemDisplay,
|
||||
type ProductItem,
|
||||
type ProductV2,
|
||||
productV2ToFrontendProduct,
|
||||
UsageModel,
|
||||
} from "@autumn/shared";
|
||||
import { useStore } from "@tanstack/react-form";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -17,7 +15,6 @@ import {
|
||||
EditPlanSection,
|
||||
FreeTrialSection,
|
||||
getFreeTrial,
|
||||
PrepaidQuantitySection,
|
||||
UpdateSubscriptionFooter,
|
||||
type UpdateSubscriptionFormContext,
|
||||
UpdateSubscriptionPreviewSection,
|
||||
@@ -42,7 +39,7 @@ import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import { useEnv } from "@/utils/envUtils";
|
||||
import { getBackendErr } from "@/utils/genUtils";
|
||||
import { getStripeInvoiceLink } from "@/utils/linkUtils";
|
||||
import { itemToFeature } from "@/utils/product/productItemUtils/convertItem";
|
||||
|
||||
import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery";
|
||||
import { useCustomerContext } from "@/views/customers2/customer/CustomerContext";
|
||||
import { InfoBox } from "@/views/onboarding2/integrate/components/InfoBox";
|
||||
@@ -98,38 +95,6 @@ function SheetContent({
|
||||
return Object.keys(changed).length > 0 ? changed : undefined;
|
||||
}, [prepaidOptions, initialPrepaidOptions]);
|
||||
|
||||
// Compute extended prepaid items that includes both original prepaid items
|
||||
// and any new prepaid items added through the inline editor
|
||||
const extendedPrepaidItems = useMemo(() => {
|
||||
if (!formValues.items) return prepaidItems;
|
||||
|
||||
// Get IDs of original prepaid items
|
||||
const originalPrepaidIds = new Set(
|
||||
prepaidItems.map((item) => item.feature_id),
|
||||
);
|
||||
|
||||
// Find new prepaid items from form that aren't in the original list
|
||||
const newPrepaidItems = formValues.items.filter(
|
||||
(item) =>
|
||||
item.usage_model === UsageModel.Prepaid &&
|
||||
item.feature_id &&
|
||||
!originalPrepaidIds.has(item.feature_id),
|
||||
);
|
||||
|
||||
// Enrich new prepaid items with feature info
|
||||
const enrichedNewItems = newPrepaidItems.map((item) => {
|
||||
const feature = itemToFeature({ item, features });
|
||||
const display = getProductItemDisplay({
|
||||
item,
|
||||
features,
|
||||
currency: "usd",
|
||||
});
|
||||
return { ...item, feature, display };
|
||||
});
|
||||
|
||||
return [...prepaidItems, ...enrichedNewItems];
|
||||
}, [prepaidItems, formValues.items, features]);
|
||||
|
||||
const productWithFormItems = useMemo((): FrontendProduct | undefined => {
|
||||
if (!product) return undefined;
|
||||
|
||||
@@ -277,25 +242,24 @@ function SheetContent({
|
||||
<EditPlanSection
|
||||
hasCustomizations={formValues.items !== null}
|
||||
onEditPlan={handleEditPlan}
|
||||
product={product}
|
||||
product={productWithFormItems as ProductV2 | undefined}
|
||||
originalItems={originalItems}
|
||||
customerProduct={customerProduct}
|
||||
features={features}
|
||||
form={form}
|
||||
numVersions={numVersions}
|
||||
currentVersion={currentVersion}
|
||||
prepaidOptions={prepaidOptions}
|
||||
initialPrepaidOptions={initialPrepaidOptions}
|
||||
/>
|
||||
|
||||
<PrepaidQuantitySection form={form} prepaidItems={extendedPrepaidItems} />
|
||||
|
||||
<FreeTrialSection form={form} customerProduct={customerProduct} />
|
||||
|
||||
<UpdateSubscriptionSummary
|
||||
form={form}
|
||||
prepaidItems={prepaidItems}
|
||||
customerProduct={customerProduct}
|
||||
currentVersion={currentVersion}
|
||||
currency={previewQuery.data?.currency}
|
||||
originalItems={originalItems}
|
||||
/>
|
||||
|
||||
<UpdateSubscriptionPreviewSection
|
||||
|
||||
Reference in New Issue
Block a user