fix: get product item full display
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Feature } from "../models/featureModels/featureModels.js";
|
||||
import { Organization } from "../models/orgModels/orgTable.js";
|
||||
import { format } from "date-fns";
|
||||
import type { Feature } from "../models/featureModels/featureModels.js";
|
||||
import type { Organization } from "../models/orgModels/orgTable.js";
|
||||
import { notNullish, nullish } from "./utils.js";
|
||||
export const getFeatureName = ({
|
||||
feature,
|
||||
@@ -20,7 +20,7 @@ export const getFeatureName = ({
|
||||
let featureName = feature.name || "";
|
||||
|
||||
if (feature.display) {
|
||||
let finalPlural;
|
||||
let finalPlural: boolean | undefined;
|
||||
// Case 1: If units and nullish plural
|
||||
if (notNullish(units) && nullish(plural)) {
|
||||
finalPlural = units !== 1;
|
||||
@@ -93,7 +93,7 @@ export const usageToFeatureName = ({
|
||||
}) => {
|
||||
const { singular, plural } = getSingularAndPlural({ feature });
|
||||
|
||||
if (usage == 1) {
|
||||
if (usage === 1) {
|
||||
return singular;
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ export const getFeatureInvoiceDescription = ({
|
||||
if (isPrepaid && billingUnits && billingUnits > 1) {
|
||||
result = `${usageStr} x ${billingUnits} ${plural}`; // eg. 4 x 100 credits
|
||||
} else {
|
||||
if (usage == 1) {
|
||||
if (usage === 1) {
|
||||
result = `${usageStr} ${singular}`; // eg. 1 credit
|
||||
} else {
|
||||
result = `${usageStr} ${plural}`; // eg. 4 credits
|
||||
@@ -148,17 +148,20 @@ export const formatAmount = ({
|
||||
amount,
|
||||
maxFractionDigits = 2,
|
||||
minFractionDigits = 0,
|
||||
amountFormatOptions,
|
||||
}: {
|
||||
org?: Organization;
|
||||
currency?: string | null;
|
||||
amount: number;
|
||||
maxFractionDigits?: number;
|
||||
minFractionDigits?: number;
|
||||
amountFormatOptions?: Intl.NumberFormatOptions;
|
||||
}) => {
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
style: "currency",
|
||||
currency: currency || org?.default_currency || "USD",
|
||||
minimumFractionDigits: minFractionDigits || 0,
|
||||
maximumFractionDigits: maxFractionDigits || 2,
|
||||
...amountFormatOptions,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
@@ -17,9 +17,9 @@ export * from "./productUtils/priceUtils.js";
|
||||
export * from "./productV2Utils/compareProductUtils.ts/compareItemUtils.js";
|
||||
export * from "./productV2Utils/compareProductUtils.ts/compareProductUtils.js";
|
||||
export * from "./productV2Utils/mapToProductV2.js";
|
||||
export * from "./productV2Utils/productItemUtils/classifyItemUtils.js";
|
||||
export * from "./productV2Utils/productItemUtils/convertItemUtils.js";
|
||||
export * from "./productV2Utils/productItemUtils/getItemType.js";
|
||||
|
||||
// Item utils
|
||||
export * from "./productV2Utils/productItemUtils/mapToItem.js";
|
||||
export * from "./productV2Utils/productItemUtils/productItemUtils.js";
|
||||
|
||||
@@ -20,9 +20,11 @@ import { notNullish, nullish } from "./utils.js";
|
||||
export const formatTiers = ({
|
||||
item,
|
||||
currency,
|
||||
amountFormatOptions,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
currency?: string | null;
|
||||
amountFormatOptions?: Intl.NumberFormatOptions;
|
||||
}) => {
|
||||
const tiers = item.tiers;
|
||||
if (tiers) {
|
||||
@@ -30,6 +32,7 @@ export const formatTiers = ({
|
||||
return formatAmount({
|
||||
currency,
|
||||
amount: tiers[0].amount,
|
||||
amountFormatOptions,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,18 +42,20 @@ export const formatTiers = ({
|
||||
return `${formatAmount({
|
||||
currency,
|
||||
amount: firstPrice,
|
||||
amountFormatOptions,
|
||||
})} - ${formatAmount({
|
||||
currency,
|
||||
amount: lastPrice,
|
||||
amountFormatOptions,
|
||||
})}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const getIntervalString = ({
|
||||
interval,
|
||||
intervalCount,
|
||||
intervalCount = 1,
|
||||
}: {
|
||||
interval: ProductItemInterval;
|
||||
interval: ProductItemInterval | null | undefined;
|
||||
intervalCount?: number | null;
|
||||
}) => {
|
||||
if (!interval) return "";
|
||||
@@ -63,22 +68,16 @@ export const getIntervalString = ({
|
||||
export const getFeatureItemDisplay = ({
|
||||
item,
|
||||
feature,
|
||||
fullDisplay = false,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
feature?: Feature;
|
||||
fullDisplay?: boolean;
|
||||
}) => {
|
||||
if (!feature) {
|
||||
throw new Error(`Feature ${item.feature_id} not found`);
|
||||
}
|
||||
// 1. If feature
|
||||
if (!feature) throw new Error(`Feature ${item.feature_id} not found`);
|
||||
|
||||
if (item.feature_type === ProductItemFeatureType.Static) {
|
||||
return {
|
||||
primary_text: getFeatureName({
|
||||
feature,
|
||||
plural: false,
|
||||
capitalize: true,
|
||||
}),
|
||||
};
|
||||
return { primary_text: feature.name };
|
||||
}
|
||||
|
||||
const featureName = getFeatureName({
|
||||
@@ -89,12 +88,22 @@ export const getFeatureItemDisplay = ({
|
||||
const includedUsageTxt =
|
||||
item.included_usage === Infinite
|
||||
? "Unlimited "
|
||||
: nullish(item.included_usage) || item.included_usage == 0
|
||||
: nullish(item.included_usage) || item.included_usage === 0
|
||||
? ""
|
||||
: `${numberWithCommas(item.included_usage!)} `;
|
||||
: `${numberWithCommas(item.included_usage)} `;
|
||||
|
||||
const intervalStr = getIntervalString({
|
||||
interval: item.interval,
|
||||
intervalCount: item.interval_count,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`feature ${feature.id}, interval ${item.interval}, interval count ${item.interval_count}, interval string ${intervalStr}`,
|
||||
);
|
||||
|
||||
return {
|
||||
primary_text: `${includedUsageTxt}${featureName}`,
|
||||
secondary_text: fullDisplay && intervalStr ? intervalStr : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -111,7 +120,7 @@ export const getPriceItemDisplay = ({
|
||||
});
|
||||
|
||||
const intervalStr = getIntervalString({
|
||||
interval: item.interval!,
|
||||
interval: item.interval,
|
||||
intervalCount: item.interval_count,
|
||||
});
|
||||
|
||||
@@ -128,13 +137,17 @@ export const getFeaturePriceItemDisplay = ({
|
||||
item,
|
||||
currency,
|
||||
isMainPrice = false,
|
||||
minifyIncluded = false,
|
||||
// minifyIncluded = false,
|
||||
amountFormatOptions,
|
||||
fullDisplay = false,
|
||||
}: {
|
||||
feature?: Feature;
|
||||
item: ProductItem;
|
||||
currency?: string | null;
|
||||
isMainPrice?: boolean;
|
||||
minifyIncluded?: boolean;
|
||||
// minifyIncluded?: boolean;
|
||||
amountFormatOptions?: Intl.NumberFormatOptions;
|
||||
fullDisplay?: boolean;
|
||||
}) => {
|
||||
if (!feature) {
|
||||
throw new Error(`Feature ${item.feature_id} not found`);
|
||||
@@ -148,15 +161,11 @@ export const getFeaturePriceItemDisplay = ({
|
||||
|
||||
const includedUsage = item.included_usage as number | null;
|
||||
let includedUsageStr = "";
|
||||
if (notNullish(includedUsage) && includedUsage! > 0) {
|
||||
if (minifyIncluded) {
|
||||
includedUsageStr = `${numberWithCommas(includedUsage!)} included`;
|
||||
} else {
|
||||
includedUsageStr = `${numberWithCommas(includedUsage!)} ${includedFeatureName}`;
|
||||
}
|
||||
if (notNullish(includedUsage) && includedUsage > 0) {
|
||||
includedUsageStr = `${numberWithCommas(includedUsage)} ${includedFeatureName}`;
|
||||
}
|
||||
|
||||
const priceStr = formatTiers({ item, currency });
|
||||
const priceStr = formatTiers({ item, currency, amountFormatOptions });
|
||||
const billingFeatureName = getFeatureName({
|
||||
feature,
|
||||
units: item.billing_units,
|
||||
@@ -170,12 +179,13 @@ export const getFeaturePriceItemDisplay = ({
|
||||
}
|
||||
|
||||
// let intervalStr = isMainPrice && item.interval ? ` per ${item.interval}` : "";
|
||||
const intervalStr = isMainPrice
|
||||
? getIntervalString({
|
||||
interval: item.interval!,
|
||||
intervalCount: item.interval_count,
|
||||
})
|
||||
: "";
|
||||
const intervalStr =
|
||||
isMainPrice || fullDisplay
|
||||
? getIntervalString({
|
||||
interval: item.interval,
|
||||
intervalCount: item.interval_count,
|
||||
})
|
||||
: "";
|
||||
|
||||
if (includedUsageStr) {
|
||||
return {
|
||||
@@ -184,7 +194,7 @@ export const getFeaturePriceItemDisplay = ({
|
||||
};
|
||||
}
|
||||
|
||||
if (isMainPrice) {
|
||||
if (isMainPrice || fullDisplay) {
|
||||
return {
|
||||
primary_text: priceStr,
|
||||
secondary_text: `per ${priceStr2} ${intervalStr}`,
|
||||
@@ -192,7 +202,7 @@ export const getFeaturePriceItemDisplay = ({
|
||||
}
|
||||
|
||||
return {
|
||||
primary_text: priceStr + ` per ${priceStr2} ${intervalStr}`,
|
||||
primary_text: `${priceStr} per ${priceStr2} ${intervalStr}`,
|
||||
secondary_text: "",
|
||||
};
|
||||
};
|
||||
@@ -201,15 +211,20 @@ export const getProductItemDisplay = ({
|
||||
item,
|
||||
features,
|
||||
currency = "usd",
|
||||
fullDisplay = false,
|
||||
amountFormatOptions,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
features: Feature[];
|
||||
currency?: string | null;
|
||||
fullDisplay?: boolean;
|
||||
amountFormatOptions?: Intl.NumberFormatOptions;
|
||||
}) => {
|
||||
if (isFeatureItem(item)) {
|
||||
return getFeatureItemDisplay({
|
||||
item,
|
||||
feature: features.find((f) => f.id === item.feature_id),
|
||||
fullDisplay,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,6 +240,8 @@ export const getProductItemDisplay = ({
|
||||
item,
|
||||
feature: features.find((f) => f.id === item.feature_id),
|
||||
currency,
|
||||
fullDisplay,
|
||||
amountFormatOptions,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,9 @@ export const productsAreSame = ({
|
||||
items1 = sanitizeItems({ items: items1, features });
|
||||
items2 = sanitizeItems({ items: items2, features });
|
||||
|
||||
// console.log("Items 1:", items1);
|
||||
// console.log("Items 2:", items2);
|
||||
|
||||
let itemsSame = true;
|
||||
let pricesChanged = false;
|
||||
const newItems: ProductItem[] = [];
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Feature } from "../../../models/featureModels/featureModels.js";
|
||||
import {
|
||||
type ProductItem,
|
||||
ProductItemFeatureType,
|
||||
} from "../../../models/productV2Models/productItemModels/productItemModels.js";
|
||||
|
||||
export const isContUseItem = ({
|
||||
item,
|
||||
features,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
features: Feature[];
|
||||
}) => {
|
||||
const feature = features.find((f) => f.id === item.feature_id);
|
||||
if (!feature) return false;
|
||||
|
||||
return feature.config?.usage_type === ProductItemFeatureType.ContinuousUse;
|
||||
};
|
||||
@@ -64,6 +64,7 @@ export interface ButtonProps
|
||||
isLoading?: boolean;
|
||||
transition?: boolean;
|
||||
disableActive?: boolean;
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
@@ -76,6 +77,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
isLoading = false,
|
||||
transition = false,
|
||||
disableActive = false,
|
||||
hide = false,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
@@ -137,6 +139,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
}
|
||||
};
|
||||
|
||||
if (hide) return null;
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={buttonRef}
|
||||
|
||||
@@ -21,16 +21,16 @@ export function PanelButton({
|
||||
data-state={isSelected ? "open" : "closed"}
|
||||
className={cn(
|
||||
// Fixed dimensions
|
||||
"w-[125px] h-[64px] relative flex items-center justify-center overflow-hidden cursor-pointer flex-shrink-0",
|
||||
"w-[144px] h-[72px] relative flex items-center justify-center overflow-hidden cursor-pointer flex-shrink-0",
|
||||
// Design system classes (following Select pattern)
|
||||
"input-base input-shadow select-bg",
|
||||
// Thicker border for panel effect
|
||||
"!rounded-xl !border-1",
|
||||
"!rounded-[0.5rem] !border-[0.09375rem]",
|
||||
// Custom panel shadows
|
||||
"shadow-[inset_0px_-8px_22px_0px_rgba(0,0,0,0.04)]",
|
||||
// Selected state shadows
|
||||
isSelected &&
|
||||
"shadow-[0px_8px_18px_20px_rgba(136,56,255,0.05)] shadow-[0px_2px_8px_0px_rgba(136,56,255,0.25)]",
|
||||
// isSelected &&
|
||||
// "shadow-[0px_8px_18px_20px_rgba(136,56,255,0.05)] shadow-[0px_2px_8px_0px_rgba(136,56,255,0.25)]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -63,11 +63,14 @@ export function PanelButton({
|
||||
|
||||
{/* Centered icon */}
|
||||
<div
|
||||
className={`size-10 rounded-lg flex items-center justify-center relative ${
|
||||
isSelected ? "bg-violet-100" : "bg-zinc-100"
|
||||
}`}
|
||||
className={cn(
|
||||
"size-9 rounded-xl flex items-center justify-center relative",
|
||||
isSelected
|
||||
? "bg-[var(--color-panel-icon-background)]"
|
||||
: "bg-zinc-100",
|
||||
)}
|
||||
>
|
||||
<div className={isSelected ? "text-violet-600" : "text-stone-500"}>
|
||||
<div className={isSelected ? "text-primary" : "text-stone-500"}>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,8 @@ interface AreaCheckboxProps {
|
||||
title: string;
|
||||
tooltip?: string;
|
||||
disabled?: boolean;
|
||||
hide?: boolean;
|
||||
description?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -29,37 +31,45 @@ function AreaCheckbox({
|
||||
title,
|
||||
tooltip,
|
||||
disabled = false,
|
||||
hide = false,
|
||||
description,
|
||||
children,
|
||||
}: AreaCheckboxProps) {
|
||||
const id = React.useId();
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!disabled && onCheckedChange) {
|
||||
onCheckedChange(!checked);
|
||||
}
|
||||
};
|
||||
|
||||
if (hide) return null;
|
||||
return (
|
||||
<div className={cn("space-y-3", className)}>
|
||||
<div className={cn("space-y-2", className)}>
|
||||
{/* Header row with checkbox, title, and tooltip */}
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={handleToggle}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleToggle();
|
||||
}
|
||||
}}
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 cursor-pointer",
|
||||
disabled && "cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
id={id}
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!disabled && onCheckedChange) {
|
||||
onCheckedChange(checked as boolean);
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
/>
|
||||
<span className="text-form-label font-medium select-none">{title}</span>
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"text-checkbox-label font-medium select-none ",
|
||||
!disabled && "hover:!text-t1",
|
||||
!checked && "opacity-50",
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
|
||||
{tooltip && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -72,16 +82,21 @@ function AreaCheckbox({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* Expanded content */}
|
||||
{children && (
|
||||
{(children || description) && (
|
||||
<div
|
||||
className={cn(
|
||||
"ml-6 space-y-4",
|
||||
"space-y-2",
|
||||
!checked && "opacity-50 pointer-events-none",
|
||||
)}
|
||||
>
|
||||
{description && (
|
||||
<p className="text-sm text-body-secondary max-w-[100%]">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button, type ButtonProps } from "../buttons/Button";
|
||||
@@ -22,6 +22,7 @@ const IconCheckbox = React.forwardRef<HTMLButtonElement, IconCheckboxProps>(
|
||||
className,
|
||||
variant = "secondary",
|
||||
size = "sm",
|
||||
hide = false,
|
||||
iconOrientation = "center",
|
||||
asChild = false,
|
||||
checked = false,
|
||||
@@ -124,12 +125,18 @@ const IconCheckbox = React.forwardRef<HTMLButtonElement, IconCheckboxProps>(
|
||||
const iconToMainClass = () => {
|
||||
switch (iconOrientation) {
|
||||
case "center":
|
||||
return "!h-6 w-6";
|
||||
if (size === "sm") {
|
||||
return "!h-6 w-6";
|
||||
} else {
|
||||
return "!h-7 w-7";
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
if (hide) return null;
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
@@ -139,7 +146,7 @@ const IconCheckbox = React.forwardRef<HTMLButtonElement, IconCheckboxProps>(
|
||||
className={cn(
|
||||
iconButtonVariants({ iconOrientation }),
|
||||
iconToMainClass(),
|
||||
"input-base input-shadow select-bg",
|
||||
"input-base input-shadow-tiny select-bg",
|
||||
className,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
@@ -155,4 +162,4 @@ const IconCheckbox = React.forwardRef<HTMLButtonElement, IconCheckboxProps>(
|
||||
|
||||
IconCheckbox.displayName = "IconCheckbox";
|
||||
|
||||
export { IconCheckbox };
|
||||
export { IconCheckbox };
|
||||
|
||||
@@ -14,7 +14,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
|
||||
// Custom classes
|
||||
// "placeholder:text-form-placeholder text-form-text rounded-lg px-2 py-1 input-border transition-none",
|
||||
"placeholder:text-t6 placeholder:select-none input-base input-shadow shadow-sm",
|
||||
"placeholder:text-t6 placeholder:select-none input-base input-shadow shadow-sm h-input",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -40,7 +40,7 @@ function SelectTrigger({
|
||||
"border-input [&_svg:not([class*='text-'])]:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent text-sm whitespace-nowrap shadow-xs outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
|
||||
// Custom border styles
|
||||
"text-sm input-base input-shadow select-bg transition-none",
|
||||
"text-sm input-base input-shadow select-bg transition-none h-input",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
|
||||
--color-hover-primary: #fcfaff;
|
||||
--color-active-primary: #f6f0ff;
|
||||
--color-panel-icon-background: #ede1ff;
|
||||
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
@@ -269,11 +270,14 @@ input[type="number"]::-webkit-inner-spin-button {
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
letter-spacing: -0.033px;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
|
||||
@@ -27,6 +27,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.input-shadow-tiny {
|
||||
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.02);
|
||||
|
||||
&:hover:not([data-disabled="true"]):not(:focus):not([data-state="open"]) {
|
||||
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&[data-state="open"] {
|
||||
box-shadow:
|
||||
0 0 0 0.2px var(--primary),
|
||||
0 4px 4px 0 rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
.input-base {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 13px;
|
||||
@@ -63,6 +78,7 @@
|
||||
background-color: var(--color-active-primary) !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&[data-state="open"],
|
||||
&[data-state="checked"] {
|
||||
background-color: var(--color-hover-primary);
|
||||
|
||||
@@ -29,6 +29,13 @@
|
||||
color: var(--color-t2);
|
||||
}
|
||||
|
||||
.text-body-highlight {
|
||||
color: var(--color-t2);
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
letter-spacing: -0.039px;
|
||||
}
|
||||
|
||||
.text-body-secondary {
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
@@ -41,6 +48,18 @@
|
||||
color: var(--color-t4);
|
||||
}
|
||||
|
||||
.text-tiny-id {
|
||||
color: var(--color-t3);
|
||||
|
||||
/* Tiny ID */
|
||||
font-family: "JetBrains Mono", "JetBrains Mono Fallback", monospace;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
letter-spacing: -0.033px;
|
||||
}
|
||||
|
||||
.text-checkbox-label {
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
@@ -76,24 +95,3 @@
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-t6);
|
||||
}
|
||||
|
||||
.text-tiny {
|
||||
color: var(--color-t4);
|
||||
/* leading-trim: both;
|
||||
text-edge: cap;
|
||||
font-family: Inter; */
|
||||
font-size: 11px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.text-tiny-id {
|
||||
color: var(--color-t3);
|
||||
|
||||
/* Tiny ID */
|
||||
font-family: "JetBrains Mono", "JetBrains Mono Fallback", monospace;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
letter-spacing: -0.033px;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
/** biome-ignore-all lint/a11y/noStaticElementInteractions: shush */
|
||||
import {
|
||||
FeatureUsageType,
|
||||
type RolloverConfig,
|
||||
RolloverDuration,
|
||||
} from "@autumn/shared";
|
||||
import { InfinityIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { FeatureUsageType } from "@autumn/shared";
|
||||
import { AreaCheckbox } from "@/components/v2/checkboxes/AreaCheckbox";
|
||||
import { IconCheckbox } from "@/components/v2/checkboxes/IconCheckbox";
|
||||
import { FormLabel } from "@/components/v2/form/FormLabel";
|
||||
import { Input } from "@/components/v2/inputs/Input";
|
||||
import {
|
||||
SheetAccordion,
|
||||
SheetAccordionItem,
|
||||
@@ -27,6 +12,8 @@ import {
|
||||
getFeatureUsageType,
|
||||
} from "@/utils/product/entitlementUtils";
|
||||
import { useProductItemContext } from "@/views/products/product/product-item/ProductItemContext";
|
||||
import { RolloverConfig } from "./advanced-settings/RolloverConfig";
|
||||
import { UsageLimit } from "./advanced-settings/UsageLimit";
|
||||
|
||||
export function AdvancedSettings() {
|
||||
const { features } = useFeaturesQuery();
|
||||
@@ -37,9 +24,6 @@ export function AdvancedSettings() {
|
||||
const usageType = getFeatureUsageType({ item, features });
|
||||
const hasCreditSystem = getFeatureCreditSystem({ item, features });
|
||||
|
||||
// Usage Limits logic
|
||||
const hasUsageLimit = item.usage_limit != null;
|
||||
|
||||
// Rollover logic
|
||||
const showRolloverConfig =
|
||||
(hasCreditSystem || usageType === FeatureUsageType.Single) &&
|
||||
@@ -47,43 +31,43 @@ export function AdvancedSettings() {
|
||||
item.included_usage &&
|
||||
Number(item.included_usage) > 0;
|
||||
|
||||
const defaultRollover: RolloverConfig = {
|
||||
duration: RolloverDuration.Month,
|
||||
length: 1 as number,
|
||||
max: null,
|
||||
};
|
||||
// const defaultRollover: RolloverConfig = {
|
||||
// duration: RolloverDuration.Month,
|
||||
// length: 1 as number,
|
||||
// max: null,
|
||||
// };
|
||||
|
||||
const setRolloverConfigKey = (
|
||||
key: keyof RolloverConfig,
|
||||
value: null | number | RolloverDuration,
|
||||
) => {
|
||||
setItem({
|
||||
...item,
|
||||
config: {
|
||||
...(item.config || {}),
|
||||
rollover: {
|
||||
...(item.config?.rollover || defaultRollover),
|
||||
[key]: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
// const setRolloverConfigKey = (
|
||||
// key: keyof RolloverConfig,
|
||||
// value: null | number | RolloverDuration,
|
||||
// ) => {
|
||||
// setItem({
|
||||
// ...item,
|
||||
// config: {
|
||||
// ...(item.config || {}),
|
||||
// rollover: {
|
||||
// ...(item.config?.rollover || defaultRollover),
|
||||
// [key]: value,
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// };
|
||||
|
||||
const setRolloverConfig = (rollover: RolloverConfig | null) => {
|
||||
const newConfig = { ...(item.config || {}) };
|
||||
if (rollover === null) {
|
||||
delete newConfig.rollover;
|
||||
} else {
|
||||
newConfig.rollover = rollover;
|
||||
}
|
||||
setItem({
|
||||
...item,
|
||||
config: newConfig,
|
||||
});
|
||||
};
|
||||
// const setRolloverConfig = (rollover: RolloverConfig | null) => {
|
||||
// const newConfig = { ...(item.config || {}) };
|
||||
// if (rollover === null) {
|
||||
// delete newConfig.rollover;
|
||||
// } else {
|
||||
// newConfig.rollover = rollover;
|
||||
// }
|
||||
// setItem({
|
||||
// ...item,
|
||||
// config: newConfig,
|
||||
// });
|
||||
// };
|
||||
|
||||
const rollover = item.config?.rollover as RolloverConfig;
|
||||
const hasRollover = item.config?.rollover != null;
|
||||
// const rollover = item.config?.rollover as RolloverConfig;
|
||||
// const hasRollover = item.config?.rollover != null;
|
||||
|
||||
return (
|
||||
<SheetAccordion type="single" withSeparator={false} collapsible={true}>
|
||||
@@ -92,11 +76,13 @@ export function AdvancedSettings() {
|
||||
title="Advanced settings"
|
||||
description="Additional configuration options for this feature"
|
||||
>
|
||||
<div className="space-y-6 pt-2">
|
||||
<div className="space-y-6 pt-2 pb-10">
|
||||
{/* Reset existing usage when product is enabled */}
|
||||
<AreaCheckbox
|
||||
title="Reset existing usage when product is enabled"
|
||||
tooltip="A customer has used 20/100 credits on a free plan. Then they upgrade to a Pro plan with 500 credits. If this flag is enabled, they'll get 500 credits on upgrade. If false, they'll have 480."
|
||||
description="When coming from another plan, this will reset the customer's feature usage to 0."
|
||||
checked={!!item.reset_usage_when_enabled}
|
||||
// hide={usageType === FeatureUsageType.Continuous}
|
||||
disabled={
|
||||
usageType === FeatureUsageType.Continuous ||
|
||||
notNullish(item.config?.rollover)
|
||||
@@ -110,49 +96,11 @@ export function AdvancedSettings() {
|
||||
/>
|
||||
|
||||
{/* Usage Limits */}
|
||||
<AreaCheckbox
|
||||
title="Usage limits"
|
||||
tooltip="Set maximum usage limits for this feature to prevent overages"
|
||||
checked={hasUsageLimit}
|
||||
onCheckedChange={(checked) => {
|
||||
let usage_limit: number | null;
|
||||
if (checked) {
|
||||
usage_limit = 100; // Default value
|
||||
} else {
|
||||
usage_limit = null;
|
||||
}
|
||||
setItem({
|
||||
...item,
|
||||
usage_limit: usage_limit,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="space-y-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
value={item.usage_limit || ""}
|
||||
className="w-32"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const numValue =
|
||||
value === "" ? null : parseInt(value) || null;
|
||||
setItem({
|
||||
...item,
|
||||
usage_limit: numValue,
|
||||
});
|
||||
}}
|
||||
placeholder="e.g. 100"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
</AreaCheckbox>
|
||||
<UsageLimit />
|
||||
|
||||
{/* Rollover */}
|
||||
{showRolloverConfig && (
|
||||
<RolloverConfig />
|
||||
{/* {showRolloverConfig && (
|
||||
<AreaCheckbox
|
||||
title="Rollovers"
|
||||
tooltip="Rollovers carry unused credits to the next billing cycle. Set a maximum rollover amount and specify how many cycles before resetting."
|
||||
@@ -263,7 +211,7 @@ export function AdvancedSettings() {
|
||||
</div>
|
||||
</div>
|
||||
</AreaCheckbox>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
</SheetAccordionItem>
|
||||
</SheetAccordion>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFeaturePriceItem } from "@autumn/shared";
|
||||
import { CoinsIcon } from "@phosphor-icons/react";
|
||||
import { PanelButton } from "@/components/v2/buttons/PanelButton";
|
||||
import { IncludedUsageIcon } from "@/components/v2/icons/AutumnIcons";
|
||||
@@ -9,8 +10,7 @@ export function BillingType() {
|
||||
if (!item) return null;
|
||||
|
||||
// Derive billing type from item state
|
||||
const billingType =
|
||||
item.tiers && item.tiers.length > 0 ? "priced" : "included";
|
||||
const isFeaturePrice = isFeaturePriceItem(item);
|
||||
|
||||
const setBillingType = (type: "included" | "priced") => {
|
||||
if (type === "included") {
|
||||
@@ -23,15 +23,15 @@ export function BillingType() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 billing-type-section">
|
||||
<div className="mt-3 space-y-4 billing-type-section">
|
||||
<div className="flex w-full items-center gap-4">
|
||||
<PanelButton
|
||||
isSelected={billingType === "included"}
|
||||
isSelected={!isFeaturePrice}
|
||||
onClick={() => setBillingType("included")}
|
||||
icon={<IncludedUsageIcon size={24} />}
|
||||
icon={<IncludedUsageIcon size={18} />}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="text-sub mb-1">Included</div>
|
||||
<div className="text-body-highlight mb-1">Included</div>
|
||||
<div className="text-body-secondary leading-tight">
|
||||
Set included usage limits with reset intervals (e.g. 100
|
||||
credits/month)
|
||||
@@ -41,12 +41,12 @@ export function BillingType() {
|
||||
|
||||
<div className="flex w-full items-center gap-4">
|
||||
<PanelButton
|
||||
isSelected={billingType === "priced"}
|
||||
isSelected={isFeaturePrice}
|
||||
onClick={() => setBillingType("priced")}
|
||||
icon={<CoinsIcon size={24} />}
|
||||
icon={<CoinsIcon size={20} />}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="text-sub mb-1">Priced</div>
|
||||
<div className="text-body-highlight mb-1">Priced</div>
|
||||
<div className="text-body-secondary leading-tight">
|
||||
Set usage and overage pricing (e.g. 100 credits/month, $1 extra)
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ProductItemFeatureType } from "@autumn/shared";
|
||||
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 { useProductItemContext } from "@/views/products/product/product-item/ProductItemContext";
|
||||
import { AdvancedSettings } from "./AdvancedSettings";
|
||||
import { BillingType } from "./BillingType";
|
||||
@@ -15,14 +16,12 @@ export function EditPlanFeatureSheet() {
|
||||
const { features } = useFeaturesQuery();
|
||||
|
||||
// Early return if no item
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
if (!item) return null;
|
||||
|
||||
const feature = getFeature(item?.feature_id ?? "", features);
|
||||
|
||||
// Derive billing type from item state - no local state needed
|
||||
const isPricedFeature = !!(item.tiers && item.tiers.length > 0);
|
||||
const isFeaturePrice = isFeaturePriceItem(item);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -33,7 +32,7 @@ export function EditPlanFeatureSheet() {
|
||||
|
||||
{item.feature_type !== ProductItemFeatureType.Static && (
|
||||
<>
|
||||
<SheetSection title="Billing type">
|
||||
<SheetSection title="Billing Type">
|
||||
<BillingType />
|
||||
</SheetSection>
|
||||
|
||||
@@ -41,7 +40,7 @@ export function EditPlanFeatureSheet() {
|
||||
<IncludedUsage />
|
||||
</SheetSection>
|
||||
|
||||
{isPricedFeature && (
|
||||
{isFeaturePrice && (
|
||||
<SheetSection title="Price">
|
||||
<PriceTiers />
|
||||
<UsageReset showBillingLabel={true} />
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
BillingInterval,
|
||||
EntInterval,
|
||||
Infinite,
|
||||
isContUseItem,
|
||||
type ProductItemInterval,
|
||||
} from "@autumn/shared";
|
||||
import { InfinityIcon } from "@phosphor-icons/react";
|
||||
@@ -22,10 +23,13 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/v2/selects/Select";
|
||||
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
|
||||
import { formatIntervalText } from "@/utils/formatUtils/formatTextUtils";
|
||||
import { isFeaturePriceItem } from "@/utils/product/getItemType";
|
||||
import { useProductItemContext } from "@/views/products/product/product-item/ProductItemContext";
|
||||
|
||||
export function IncludedUsage() {
|
||||
const { features } = useFeaturesQuery();
|
||||
const { item, setItem } = useProductItemContext();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@@ -52,9 +56,7 @@ export function IncludedUsage() {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
// Determine billing type
|
||||
const billingType =
|
||||
item.tiers && item.tiers.length > 0 ? "priced" : "included";
|
||||
const isFeaturePrice = isFeaturePriceItem(item);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -81,6 +83,7 @@ export function IncludedUsage() {
|
||||
disabled={includedUsage === Infinite}
|
||||
/>
|
||||
<IconCheckbox
|
||||
hide={isFeaturePrice}
|
||||
icon={<InfinityIcon />}
|
||||
iconOrientation="center"
|
||||
variant="muted"
|
||||
@@ -100,7 +103,7 @@ export function IncludedUsage() {
|
||||
</div>
|
||||
|
||||
{/* Only show Usage Reset dropdown for included billing type */}
|
||||
{billingType === "included" && (
|
||||
{!isFeaturePrice && !isContUseItem({ item, features }) && (
|
||||
<div>
|
||||
<div className="text-form-label block mb-2">Usage Reset</div>
|
||||
<Select
|
||||
|
||||
@@ -54,7 +54,6 @@ export function PriceTiers() {
|
||||
onClick={() => addTier({ item, setItem })}
|
||||
icon={<Plus size={12} />}
|
||||
iconOrientation="left"
|
||||
className="p-1"
|
||||
>
|
||||
Add Tiers
|
||||
</IconButton>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { UsageModel } from "@autumn/shared";
|
||||
import { LongCheckbox } from "@/components/v2/checkboxes/LongCheckbox";
|
||||
import { AreaCheckbox } from "@/components/v2/checkboxes/AreaCheckbox";
|
||||
import { useProductItemContext } from "@/views/products/product/product-item/ProductItemContext";
|
||||
|
||||
export function PricedFeatureSettings() {
|
||||
@@ -11,9 +11,9 @@ export function PricedFeatureSettings() {
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<LongCheckbox
|
||||
<AreaCheckbox
|
||||
title="Prepaid"
|
||||
subtitle="Quantity will be chosen during checkout."
|
||||
description="Quantity will be chosen during checkout."
|
||||
checked={prepaid}
|
||||
onCheckedChange={(checked) => {
|
||||
const newUsageModel = checked
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import {
|
||||
FeatureUsageType,
|
||||
type RolloverConfig as RolloverConfigType,
|
||||
RolloverDuration,
|
||||
} from "@autumn/shared";
|
||||
import { InfinityIcon } from "@phosphor-icons/react";
|
||||
import { AreaCheckbox } from "@/components/v2/checkboxes/AreaCheckbox";
|
||||
import { IconCheckbox } from "@/components/v2/checkboxes/IconCheckbox";
|
||||
import { FormLabel } from "@/components/v2/form/FormLabel";
|
||||
import { Input } from "@/components/v2/inputs/Input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/v2/selects/Select";
|
||||
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
|
||||
import {
|
||||
getFeatureCreditSystem,
|
||||
getFeatureUsageType,
|
||||
} from "@/utils/product/entitlementUtils";
|
||||
import { useProductItemContext } from "@/views/products/product/product-item/ProductItemContext";
|
||||
|
||||
export function RolloverConfig() {
|
||||
const { features } = useFeaturesQuery();
|
||||
const { item, setItem } = useProductItemContext();
|
||||
|
||||
if (!item) return null;
|
||||
|
||||
const usageType = getFeatureUsageType({ item, features });
|
||||
const hasCreditSystem = getFeatureCreditSystem({ item, features });
|
||||
|
||||
// Rollover logic
|
||||
const showRolloverConfig =
|
||||
(hasCreditSystem || usageType === FeatureUsageType.Single) &&
|
||||
item.interval !== null &&
|
||||
item.included_usage &&
|
||||
Number(item.included_usage) > 0;
|
||||
|
||||
const defaultRollover: RolloverConfigType = {
|
||||
duration: RolloverDuration.Month,
|
||||
length: 1 as number,
|
||||
max: null,
|
||||
};
|
||||
|
||||
const setRolloverConfigKey = (
|
||||
key: keyof RolloverConfigType,
|
||||
value: null | number | RolloverDuration,
|
||||
) => {
|
||||
setItem({
|
||||
...item,
|
||||
config: {
|
||||
...(item.config || {}),
|
||||
rollover: {
|
||||
...(item.config?.rollover || defaultRollover),
|
||||
[key]: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const setRolloverConfig = (rollover: RolloverConfigType | null) => {
|
||||
const newConfig = { ...(item.config || {}) };
|
||||
if (rollover === null) {
|
||||
delete newConfig.rollover;
|
||||
} else {
|
||||
newConfig.rollover = rollover;
|
||||
}
|
||||
setItem({
|
||||
...item,
|
||||
config: newConfig,
|
||||
});
|
||||
};
|
||||
|
||||
const rollover = item.config?.rollover as RolloverConfigType;
|
||||
const hasRollover = item.config?.rollover != null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showRolloverConfig && (
|
||||
<AreaCheckbox
|
||||
title="Rollovers"
|
||||
tooltip="Rollovers carry unused credits to the next billing cycle. Set a maximum rollover amount and specify how many cycles before resetting."
|
||||
checked={hasRollover}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setItem({
|
||||
...item,
|
||||
reset_usage_when_enabled: true,
|
||||
config: {
|
||||
...(item.config || {}),
|
||||
rollover: defaultRollover,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setRolloverConfig(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="space-y-4"
|
||||
// onClick={(e) => e.stopPropagation()}
|
||||
// onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<FormLabel>Maximum rollover amount</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
value={
|
||||
rollover?.max === null
|
||||
? ""
|
||||
: rollover?.max === 0
|
||||
? ""
|
||||
: rollover?.max
|
||||
}
|
||||
className="w-32"
|
||||
placeholder={
|
||||
rollover?.max === null ? "Unlimited" : "e.g. 100 credits"
|
||||
}
|
||||
disabled={rollover?.max === null}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const numValue = value === "" ? 0 : parseInt(value) || 0;
|
||||
setRolloverConfigKey("max", numValue);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<IconCheckbox
|
||||
icon={<InfinityIcon />}
|
||||
iconOrientation="center"
|
||||
variant="muted"
|
||||
size="default"
|
||||
checked={rollover?.max === null}
|
||||
onCheckedChange={(checked) =>
|
||||
setRolloverConfigKey("max", checked ? null : 0)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<FormLabel>Rollover duration</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
{rollover?.duration === RolloverDuration.Month && (
|
||||
<Input
|
||||
type="number"
|
||||
value={rollover?.length === 0 ? "" : rollover?.length || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const numValue = value === "" ? 0 : parseInt(value) || 0;
|
||||
setRolloverConfigKey("length", numValue);
|
||||
}}
|
||||
className="w-32"
|
||||
placeholder="e.g. 1 month"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
<Select
|
||||
value={rollover?.duration}
|
||||
onValueChange={(value) => {
|
||||
setRolloverConfigKey("duration", value as RolloverDuration);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-32"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<SelectValue placeholder="Select duration" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.values(RolloverDuration).map((duration) => (
|
||||
<SelectItem key={duration} value={duration}>
|
||||
{duration}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AreaCheckbox>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { notNullish } from "@autumn/shared";
|
||||
import { AreaCheckbox } from "@/components/v2/checkboxes/AreaCheckbox";
|
||||
import { Input } from "@/components/v2/inputs/Input";
|
||||
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
|
||||
import {
|
||||
getFeatureCreditSystem,
|
||||
getFeatureUsageType,
|
||||
} from "@/utils/product/entitlementUtils";
|
||||
import { useProductItemContext } from "@/views/products/product/product-item/ProductItemContext";
|
||||
|
||||
export function UsageLimit() {
|
||||
const { features } = useFeaturesQuery();
|
||||
const { item, setItem } = useProductItemContext();
|
||||
|
||||
if (!item) return null;
|
||||
|
||||
const usageType = getFeatureUsageType({ item, features });
|
||||
const hasCreditSystem = getFeatureCreditSystem({ item, features });
|
||||
|
||||
return (
|
||||
<AreaCheckbox
|
||||
title="Usage limit"
|
||||
// tooltip="Set maximum usage limits for this feature to prevent overages"
|
||||
description="The maximum total amount of this feature a customer can use, including
|
||||
their included usage."
|
||||
checked={notNullish(item.usage_limit)}
|
||||
onCheckedChange={(checked) => {
|
||||
let usage_limit: number | null;
|
||||
|
||||
if (checked) {
|
||||
usage_limit = 100; // Default value
|
||||
} else {
|
||||
usage_limit = null;
|
||||
}
|
||||
|
||||
console.log("checked", checked, "setting usage limit to", usage_limit);
|
||||
|
||||
setItem({
|
||||
...item,
|
||||
usage_limit: usage_limit,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
value={item.usage_limit || ""}
|
||||
className="w-32"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const numValue = value === "" ? 0 : parseInt(value) || null;
|
||||
setItem({
|
||||
...item,
|
||||
usage_limit: numValue,
|
||||
});
|
||||
}}
|
||||
placeholder="e.g. 100"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</AreaCheckbox>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { ProductItem } from "@autumn/shared";
|
||||
import { getProductItemDisplay } from "@autumn/shared";
|
||||
import { TrashIcon } from "@phosphor-icons/react";
|
||||
import { useState } from "react";
|
||||
import { CopyButton } from "@/components/v2/buttons/CopyButton";
|
||||
import { IconButton } from "@/components/v2/buttons/IconButton";
|
||||
import { useOrg } from "@/hooks/common/useOrg";
|
||||
@@ -35,16 +36,15 @@ export const PlanFeatureRow = ({
|
||||
const { org } = useOrg();
|
||||
const { features } = useFeaturesQuery();
|
||||
const { editingState } = useProductContext();
|
||||
const [isPressed, setIsPressed] = useState(false);
|
||||
|
||||
const getDisplayText = (item: ProductItem) => {
|
||||
const displayData = getProductItemDisplay({
|
||||
item,
|
||||
features,
|
||||
currency: org?.default_currency || "USD",
|
||||
});
|
||||
|
||||
return displayData.primary_text;
|
||||
};
|
||||
const display = getProductItemDisplay({
|
||||
item,
|
||||
features,
|
||||
currency: org?.default_currency || "USD",
|
||||
fullDisplay: true,
|
||||
amountFormatOptions: { currencyDisplay: "narrowSymbol" },
|
||||
});
|
||||
|
||||
const isSelected = getItemId({ item, itemIndex: index }) === editingState.id;
|
||||
|
||||
@@ -52,13 +52,28 @@ export const PlanFeatureRow = ({
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
data-state={isSelected ? "open" : "closed"}
|
||||
data-pressed={isPressed}
|
||||
className={cn(
|
||||
"flex w-full group !h-9 group/row input-base btn-secondary-shadow",
|
||||
"flex w-full group !h-9 group/row input-base input-shadow-tiny select-bg",
|
||||
|
||||
// To prevent flickering when clicking inner buttons
|
||||
!isSelected &&
|
||||
"hover:!bg-hover-primary focus-visible:!bg-hover-primary focus-visible:!border-primary",
|
||||
isSelected &&
|
||||
"!bg-active-primary !border-primary !shadow-[0px_0px_0px_0.2px_var(--color-primary)]",
|
||||
|
||||
isSelected && "!bg-hover-primary !border-primary",
|
||||
|
||||
// Custom pressed state that we can control
|
||||
"data-[pressed=true]:!bg-active-primary data-[pressed=true]:border-primary",
|
||||
)}
|
||||
onMouseDown={(e) => {
|
||||
// Only set pressed if we're not clicking on a button
|
||||
if (!(e.target as Element).closest("button")) {
|
||||
setIsPressed(true);
|
||||
}
|
||||
}}
|
||||
onMouseUp={() => setIsPressed(false)}
|
||||
onMouseLeave={() => setIsPressed(false)}
|
||||
onClick={() => onEdit?.(item)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
@@ -68,25 +83,30 @@ export const PlanFeatureRow = ({
|
||||
}}
|
||||
>
|
||||
{/* Left side - Icons and text */}
|
||||
<div className="flex flex-row items-center flex-1 gap-4 min-w-0">
|
||||
<div className="flex flex-row items-center flex-1 gap-4 min-w-0 relative">
|
||||
<div className="flex flex-row items-center gap-1 flex-shrink-0">
|
||||
<PlanFeatureIcon item={item} position="left" />
|
||||
<CustomDotIcon />
|
||||
<PlanFeatureIcon item={item} position="right" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span className="text-t2 font-medium whitespace-nowrap font-inter text-[13px] leading-4 tracking-[-0.003em]">
|
||||
{getDisplayText(item)}
|
||||
</span>
|
||||
<CopyButton
|
||||
text={item.feature_id || ""}
|
||||
disableActive={true}
|
||||
size="sm"
|
||||
variant="skeleton"
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity duration-50"
|
||||
/>
|
||||
<div className="flex items-center gap-2 flex-1 max-w-[85%]">
|
||||
<p className="whitespace-nowrap truncate">
|
||||
<span className="text-body">{display.primary_text}</span>
|
||||
<span className="text-body-secondary">
|
||||
{" "}
|
||||
{display.secondary_text}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<CopyButton
|
||||
// hide={true}
|
||||
text={item.feature_id || ""}
|
||||
disableActive={true}
|
||||
size="sm"
|
||||
variant="skeleton"
|
||||
className="absolute right-0 z-20 opacity-0 group-hover:opacity-100 transition-opacity duration-50 bg-hover-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-50">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LongCheckbox } from "@/components/v2/checkboxes/LongCheckbox";
|
||||
import { AreaCheckbox } from "@/components/v2/checkboxes/AreaCheckbox";
|
||||
import { SheetSection } from "@/components/v2/sheets/InlineSheet";
|
||||
import { useProductContext } from "@/views/products/product/ProductContext";
|
||||
|
||||
@@ -10,18 +10,18 @@ export const AdditionalOptions = () => {
|
||||
return (
|
||||
<SheetSection title="Additional Options">
|
||||
<div className="space-y-4">
|
||||
<LongCheckbox
|
||||
<AreaCheckbox
|
||||
title="Default"
|
||||
subtitle="This product will be enabled by default for all new users,
|
||||
description="This product will be enabled by default for all new users,
|
||||
typically used for your free plan"
|
||||
checked={product.is_default}
|
||||
onCheckedChange={(checked) =>
|
||||
setProduct({ ...product, is_default: checked })
|
||||
}
|
||||
/>
|
||||
<LongCheckbox
|
||||
<AreaCheckbox
|
||||
title="Add On"
|
||||
subtitle="This product is an add-on that can be bought together with your
|
||||
description="This product is an add-on that can be bought together with your
|
||||
base products (eg, top ups)"
|
||||
checked={product.is_add_on}
|
||||
onCheckedChange={(checked) =>
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import FieldLabel from "@/components/general/modal-components/FieldLabel";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Infinite,
|
||||
import {
|
||||
BillingInterval,
|
||||
EntInterval,
|
||||
ProductItemInterval,
|
||||
Infinite,
|
||||
UsageModel,
|
||||
} from "@autumn/shared";
|
||||
import { useState } from "react";
|
||||
import FieldLabel from "@/components/general/modal-components/FieldLabel";
|
||||
import { InfoTooltip } from "@/components/general/modal-components/InfoTooltip";
|
||||
import { ToggleDisplayButton } from "@/components/general/ToggleDisplayButton";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -15,18 +21,11 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import { useProductItemContext } from "../../ProductItemContext";
|
||||
import { ToggleDisplayButton } from "@/components/general/ToggleDisplayButton";
|
||||
import { itemIsUnlimited } from "@/utils/product/productItemUtils";
|
||||
import { isFeaturePriceItem } from "@/utils/product/getItemType";
|
||||
import { InfoTooltip } from "@/components/general/modal-components/InfoTooltip";
|
||||
import { formatIntervalText } from "@/utils/formatUtils/formatTextUtils";
|
||||
import { isFeaturePriceItem } from "@/utils/product/getItemType";
|
||||
import { itemIsUnlimited } from "@/utils/product/productItemUtils";
|
||||
import { useProductItemContext } from "../../ProductItemContext";
|
||||
|
||||
export const IncludedUsage = () => {
|
||||
const { item, setItem } = useProductItemContext();
|
||||
@@ -36,7 +35,9 @@ export const IncludedUsage = () => {
|
||||
item.interval_count || 1,
|
||||
);
|
||||
|
||||
const handleBillingIntervalSelected = (value: BillingInterval | EntInterval) => {
|
||||
const handleBillingIntervalSelected = (
|
||||
value: BillingInterval | EntInterval,
|
||||
) => {
|
||||
let usageModel = item.usage_model;
|
||||
if (value === BillingInterval.OneOff) {
|
||||
usageModel = UsageModel.Prepaid;
|
||||
@@ -44,7 +45,10 @@ export const IncludedUsage = () => {
|
||||
|
||||
setItem({
|
||||
...item,
|
||||
interval: value === BillingInterval.OneOff || value === EntInterval.Lifetime ? null : value,
|
||||
interval:
|
||||
value === BillingInterval.OneOff || value === EntInterval.Lifetime
|
||||
? null
|
||||
: value,
|
||||
usage_model: usageModel,
|
||||
});
|
||||
};
|
||||
@@ -162,7 +166,9 @@ export const IncludedUsage = () => {
|
||||
<Button
|
||||
className="w-full justify-start px-2"
|
||||
variant="skeleton"
|
||||
disabled={item.included_usage === Infinite || item.interval == null}
|
||||
disabled={
|
||||
item.included_usage === Infinite || item.interval == null
|
||||
}
|
||||
>
|
||||
<p className="text-t3">Customise Interval</p>
|
||||
</Button>
|
||||
@@ -191,7 +197,11 @@ export const IncludedUsage = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button variant="secondary" className="px-4 h-7" onClick={handleSaveCustomInterval}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="px-4 h-7"
|
||||
onClick={handleSaveCustomInterval}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user