diff --git a/shared/utils/displayUtils.ts b/shared/utils/displayUtils.ts index bf27a4dbf..64e66ec3a 100644 --- a/shared/utils/displayUtils.ts +++ b/shared/utils/displayUtils.ts @@ -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); }; diff --git a/shared/utils/index.ts b/shared/utils/index.ts index 8ea35d7ab..1b09d39d2 100644 --- a/shared/utils/index.ts +++ b/shared/utils/index.ts @@ -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"; diff --git a/shared/utils/productDisplayUtils.ts b/shared/utils/productDisplayUtils.ts index fecd9312e..634746868 100644 --- a/shared/utils/productDisplayUtils.ts +++ b/shared/utils/productDisplayUtils.ts @@ -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, }); } diff --git a/shared/utils/productV2Utils/compareProductUtils.ts/compareProductUtils.ts b/shared/utils/productV2Utils/compareProductUtils.ts/compareProductUtils.ts index 6dc0580be..4f95b4f31 100644 --- a/shared/utils/productV2Utils/compareProductUtils.ts/compareProductUtils.ts +++ b/shared/utils/productV2Utils/compareProductUtils.ts/compareProductUtils.ts @@ -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[] = []; diff --git a/shared/utils/productV2Utils/productItemUtils/classifyItemUtils.ts b/shared/utils/productV2Utils/productItemUtils/classifyItemUtils.ts new file mode 100644 index 000000000..81d909fb1 --- /dev/null +++ b/shared/utils/productV2Utils/productItemUtils/classifyItemUtils.ts @@ -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; +}; diff --git a/vite/src/components/v2/buttons/Button.tsx b/vite/src/components/v2/buttons/Button.tsx index 4ef4841d1..d93f4d146 100644 --- a/vite/src/components/v2/buttons/Button.tsx +++ b/vite/src/components/v2/buttons/Button.tsx @@ -64,6 +64,7 @@ export interface ButtonProps isLoading?: boolean; transition?: boolean; disableActive?: boolean; + hide?: boolean; } const Button = React.forwardRef( @@ -76,6 +77,7 @@ const Button = React.forwardRef( isLoading = false, transition = false, disableActive = false, + hide = false, children, ...props }, @@ -137,6 +139,8 @@ const Button = React.forwardRef( } }; + if (hide) return null; + return ( @@ -63,11 +63,14 @@ export function PanelButton({ {/* Centered icon */}
-
+
{icon}
diff --git a/vite/src/components/v2/checkboxes/AreaCheckbox.tsx b/vite/src/components/v2/checkboxes/AreaCheckbox.tsx index 51c91b710..50dde08ad 100644 --- a/vite/src/components/v2/checkboxes/AreaCheckbox.tsx +++ b/vite/src/components/v2/checkboxes/AreaCheckbox.tsx @@ -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 ( -
+
{/* Header row with checkbox, title, and tooltip */} -
{ - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - handleToggle(); - } - }} +
+ {/* Expanded content */} - {children && ( + {(children || description) && (
+ {description && ( +

+ {description} +

+ )} {children}
)} diff --git a/vite/src/components/v2/checkboxes/IconCheckbox.tsx b/vite/src/components/v2/checkboxes/IconCheckbox.tsx index 5cbd945e0..404e09ec2 100644 --- a/vite/src/components/v2/checkboxes/IconCheckbox.tsx +++ b/vite/src/components/v2/checkboxes/IconCheckbox.tsx @@ -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( className, variant = "secondary", size = "sm", + hide = false, iconOrientation = "center", asChild = false, checked = false, @@ -124,12 +125,18 @@ const IconCheckbox = React.forwardRef( 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 ( ( 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( IconCheckbox.displayName = "IconCheckbox"; -export { IconCheckbox }; \ No newline at end of file +export { IconCheckbox }; diff --git a/vite/src/components/v2/inputs/Input.tsx b/vite/src/components/v2/inputs/Input.tsx index 0126fe4d2..f248c4755 100644 --- a/vite/src/components/v2/inputs/Input.tsx +++ b/vite/src/components/v2/inputs/Input.tsx @@ -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} diff --git a/vite/src/components/v2/selects/Select.tsx b/vite/src/components/v2/selects/Select.tsx index db04a6755..22fb3d0b8 100644 --- a/vite/src/components/v2/selects/Select.tsx +++ b/vite/src/components/v2/selects/Select.tsx @@ -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} diff --git a/vite/src/index.css b/vite/src/index.css index a160d887c..58747a1ab 100644 --- a/vite/src/index.css +++ b/vite/src/index.css @@ -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; diff --git a/vite/src/styles/custom.css b/vite/src/styles/custom.css index bdd9f08ba..c618127b3 100644 --- a/vite/src/styles/custom.css +++ b/vite/src/styles/custom.css @@ -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); diff --git a/vite/src/styles/typography.css b/vite/src/styles/typography.css index 534bb415e..e69410c16 100644 --- a/vite/src/styles/typography.css +++ b/vite/src/styles/typography.css @@ -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; -} diff --git a/vite/src/views/products/plan/components/EditPlanFeatureSheet/AdvancedSettings.tsx b/vite/src/views/products/plan/components/EditPlanFeatureSheet/AdvancedSettings.tsx index 8f10f5a85..0c3ec3685 100644 --- a/vite/src/views/products/plan/components/EditPlanFeatureSheet/AdvancedSettings.tsx +++ b/vite/src/views/products/plan/components/EditPlanFeatureSheet/AdvancedSettings.tsx @@ -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 ( @@ -92,11 +76,13 @@ export function AdvancedSettings() { title="Advanced settings" description="Additional configuration options for this feature" > -
+
+ {/* Reset existing usage when product is enabled */} {/* Usage Limits */} - { - let usage_limit: number | null; - if (checked) { - usage_limit = 100; // Default value - } else { - usage_limit = null; - } - setItem({ - ...item, - usage_limit: usage_limit, - }); - }} - > -
e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - > - { - 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()} - /> -
-
+ {/* Rollover */} - {showRolloverConfig && ( + + {/* {showRolloverConfig && (
- )} + )} */}
diff --git a/vite/src/views/products/plan/components/EditPlanFeatureSheet/BillingType.tsx b/vite/src/views/products/plan/components/EditPlanFeatureSheet/BillingType.tsx index 6fb7ee56d..a745f4299 100644 --- a/vite/src/views/products/plan/components/EditPlanFeatureSheet/BillingType.tsx +++ b/vite/src/views/products/plan/components/EditPlanFeatureSheet/BillingType.tsx @@ -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 ( -
+
setBillingType("included")} - icon={} + icon={} />
-
Included
+
Included
Set included usage limits with reset intervals (e.g. 100 credits/month) @@ -41,12 +41,12 @@ export function BillingType() {
setBillingType("priced")} - icon={} + icon={} />
-
Priced
+
Priced
Set usage and overage pricing (e.g. 100 credits/month, $1 extra)
diff --git a/vite/src/views/products/plan/components/EditPlanFeatureSheet/EditPlanFeatureSheet.tsx b/vite/src/views/products/plan/components/EditPlanFeatureSheet/EditPlanFeatureSheet.tsx index df78958e3..c8a15a82a 100644 --- a/vite/src/views/products/plan/components/EditPlanFeatureSheet/EditPlanFeatureSheet.tsx +++ b/vite/src/views/products/plan/components/EditPlanFeatureSheet/EditPlanFeatureSheet.tsx @@ -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 && ( <> - + @@ -41,7 +40,7 @@ export function EditPlanFeatureSheet() { - {isPricedFeature && ( + {isFeaturePrice && ( diff --git a/vite/src/views/products/plan/components/EditPlanFeatureSheet/IncludedUsage.tsx b/vite/src/views/products/plan/components/EditPlanFeatureSheet/IncludedUsage.tsx index 6919f1522..3184b7e3d 100644 --- a/vite/src/views/products/plan/components/EditPlanFeatureSheet/IncludedUsage.tsx +++ b/vite/src/views/products/plan/components/EditPlanFeatureSheet/IncludedUsage.tsx @@ -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 (
@@ -81,6 +83,7 @@ export function IncludedUsage() { disabled={includedUsage === Infinite} /> } iconOrientation="center" variant="muted" @@ -100,7 +103,7 @@ export function IncludedUsage() {
{/* Only show Usage Reset dropdown for included billing type */} - {billingType === "included" && ( + {!isFeaturePrice && !isContUseItem({ item, features }) && (
Usage Reset
{ + const value = e.target.value; + const numValue = value === "" ? 0 : parseInt(value) || 0; + setRolloverConfigKey("max", numValue); + }} + onClick={(e) => e.stopPropagation()} + /> + } + iconOrientation="center" + variant="muted" + size="default" + checked={rollover?.max === null} + onCheckedChange={(checked) => + setRolloverConfigKey("max", checked ? null : 0) + } + /> +
+
+ +
+ Rollover duration +
+ {rollover?.duration === RolloverDuration.Month && ( + { + 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()} + /> + )} + +
+
+
+ + )} + + ); +} diff --git a/vite/src/views/products/plan/components/EditPlanFeatureSheet/advanced-settings/UsageLimit.tsx b/vite/src/views/products/plan/components/EditPlanFeatureSheet/advanced-settings/UsageLimit.tsx new file mode 100644 index 000000000..f7dba1ae3 --- /dev/null +++ b/vite/src/views/products/plan/components/EditPlanFeatureSheet/advanced-settings/UsageLimit.tsx @@ -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 ( + { + 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, + }); + }} + > + { + 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()} + /> + + ); +} diff --git a/vite/src/views/products/plan/components/PlanCard/PlanFeatureRow.tsx b/vite/src/views/products/plan/components/PlanCard/PlanFeatureRow.tsx index 3aa4bc4ed..d23d5674d 100644 --- a/vite/src/views/products/plan/components/PlanCard/PlanFeatureRow.tsx +++ b/vite/src/views/products/plan/components/PlanCard/PlanFeatureRow.tsx @@ -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 = ({
{ + // 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 */} -
+
-
- - {getDisplayText(item)} - - +
+

+ {display.primary_text} + + {" "} + {display.secondary_text} + +

+
diff --git a/vite/src/views/products/plan/components/edit-plan-details/AdditionalOptions.tsx b/vite/src/views/products/plan/components/edit-plan-details/AdditionalOptions.tsx index c36171c95..aeddaa1ad 100644 --- a/vite/src/views/products/plan/components/edit-plan-details/AdditionalOptions.tsx +++ b/vite/src/views/products/plan/components/edit-plan-details/AdditionalOptions.tsx @@ -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 (
- setProduct({ ...product, is_default: checked }) } /> - diff --git a/vite/src/views/products/product/product-item/product-item-config/components/IncludedUsage.tsx b/vite/src/views/products/product/product-item/product-item-config/components/IncludedUsage.tsx index 7bca6adfe..394921277 100644 --- a/vite/src/views/products/product/product-item/product-item-config/components/IncludedUsage.tsx +++ b/vite/src/views/products/product/product-item/product-item-config/components/IncludedUsage.tsx @@ -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 = () => { @@ -191,7 +197,11 @@ export const IncludedUsage = () => { } }} /> -