Made onboarding dark mode and fixed to new plan editor.

This commit is contained in:
Ayush Rodrigues
2025-11-18 16:59:32 +00:00
parent 53d4b95106
commit 2fa9767d30
26 changed files with 117 additions and 92 deletions

View File

@@ -1,11 +1,10 @@
// 1. productV2ToPlanType
import type { ProductItem } from "@models/productV2Models/productItemModels/productItemModels.js";
import type {
FrontendProduct,
ProductV2,
} from "@models/productV2Models/productV2Models.js";
import { isFeatureItem, isFeaturePriceItem, notNullish } from "@utils/index.js";
import { isFeaturePriceItem, notNullish } from "@utils/index.js";
import { productV2ToBasePrice } from "@utils/productV3Utils/productItemUtils/productV3ItemUtils.js";
// 2. productV2ToBasePriceType
@@ -40,7 +39,7 @@ export const productV2ToFrontendProduct = ({
return {
...product,
planType:
hasPricedFeatures || notNullish(basePrice?.amount) ? "paid" : "free",
hasPricedFeatures || notNullish(basePrice?.price) ? "paid" : "free",
basePriceType: basePrice?.interval
? "recurring"
: basePrice

View File

@@ -32,7 +32,7 @@ const PlanFeatureRowPreview = ({ item }: { item: ProductItem }) => {
const display = item.display || { primary_text: "", secondary_text: "" };
return (
<div className="flex w-full h-9 items-center px-2 rounded-lg border border-border bg-card">
<div className="flex w-full h-9 items-center px-2 rounded-lg border bg-card">
{/* Left side - Icons and text */}
<div className="flex flex-row items-center flex-1 gap-3 min-w-0">
<div className="flex flex-row items-center gap-1 flex-shrink-0">
@@ -73,7 +73,7 @@ export const PlanCardPreview = ({
return (
<Card
className={cn(
"min-w-[280px] max-w-md bg-card flex flex-col border-none shadow-[inset_0_0_0_0.5px_var(--t10)] !gap-0",
"min-w-[280px] max-w-md bg-card flex flex-col border !gap-0",
recommended && "ring-2 ring-primary",
)}
>
@@ -81,7 +81,6 @@ export const PlanCardPreview = ({
<div className="flex flex-col gap-2">
{/* Title */}
<h3 className="text-sub">{product.name}</h3>
{/* Price */}
{product.items.filter((x) => isFeaturePriceItem(x as ProductItem))
.length > 0 &&
@@ -97,7 +96,6 @@ export const PlanCardPreview = ({
) : (
<span className="text-main">Free</span>
)}
{/* Description */}
{product.description && (
<p className="text-body-secondary line-clamp-2">
@@ -121,7 +119,6 @@ export const PlanCardPreview = ({
))}
</div>
)}
{/* Action button */}
{/* <Tooltip> */}
{/* <TooltipTrigger asChild> */}

View File

@@ -18,7 +18,7 @@ export const CustomToaster = () => {
toastOptions={{
duration: 6000,
className:
"w-96 px-3 pt-3 pb-2.5 bg-white rounded-xl shadow-[0_4px_4px_0_rgba(0,0,0,0.02),inset_0_-4px_6px_0_rgba(0,0,0,0.04),0_4px_24px_0_rgba(0,0,0,0.03)] outline outline-16 inline-flex flex-row justify-start items-start gap-3 overflow-hidden",
"w-96 px-3 pt-3 pb-2.5 rounded-xl shadow-[0_4px_4px_0_rgba(0,0,0,0.02),inset_0_-4px_6px_0_rgba(0,0,0,0.04),0_4px_24px_0_rgba(0,0,0,0.03)] outline outline-16 inline-flex flex-row justify-start items-start gap-3 overflow-hidden !bg-card !text-t1",
style: {
"--normal-border": "var(--t10)",
} as React.CSSProperties,

View File

@@ -45,11 +45,11 @@ const CodeGroupTab = React.forwardRef<
ref={ref}
className={cn(
"flex items-center justify-center flex-1 min-w-0 h-6 px-2 py-1 text-[13px] font-semibold tracking-[-0.039px] leading-normal whitespace-nowrap",
"bg-white border border-t10 border-r-0",
"text-t12 transition-none outline-none",
"bg-interactive-secondary border border-r-0",
"text-t3 transition-none outline-none",
"hover:text-primary",
"focus-visible:text-primary",
"data-[state=active]:bg-neutral-50 data-[state=active]:shadow-[0px_3px_4px_0px_inset_rgba(0,0,0,0.04)]",
"data-[state=active]:bg-interactive-secondary-hover data-[state=active]:shadow-[0px_3px_4px_0px_inset_rgba(0,0,0,0.04)]",
"data-[state=inactive]:shadow-[0px_-3px_4px_0px_inset_rgba(0,0,0,0.04)]",
"first:rounded-tl-md first:border-l",
className,
@@ -87,7 +87,7 @@ const CodeGroupCopyButton = React.forwardRef<
onClick={handleClick}
className={cn(
"flex items-center justify-center h-6 px-2 py-1",
"bg-white border border-t10 rounded-tr-md",
"bg-interactive-secondary border rounded-tr-md",
"shadow-[0px_-3px_4px_0px_inset_rgba(0,0,0,0.04)]",
"hover:text-primary transition-none outline-none",
"focus-visible:text-primary",
@@ -180,7 +180,7 @@ const CodeGroupContent = React.forwardRef<
<TabsPrimitive.Content
ref={ref}
className={cn(
"bg-white border border-t10 border-t-0 rounded-bl-lg rounded-br-lg",
"bg-white dark:bg-background border border-t-0 rounded-bl-lg rounded-br-lg",
"p-4 outline-none",
"cursor-pointer transition-colors",
"hover:bg-neutral-50/50",

View File

@@ -38,8 +38,8 @@ export default function StackBadge({
className={cn(
"pr-2 rounded-md shadow-[0px_4px_4px_0px_rgba(0,0,0,0.02)] shadow-[inset_0px_-3px_4px_0px_rgba(0,0,0,0.04)] outline outline-1 outline-offset-[-1px] inline-flex justify-start items-center gap-1.5 transition-none",
isSelected
? "bg-white outline-primary"
: "bg-white outline-neutral-300",
? "bg-interactive-secondary outline-primary"
: "bg-interactive-secondary outline-border",
isClickable && !isSelected && "hover:outline-primary cursor-pointer",
isClickable && "focus-visible:outline-primary focus-visible:outline-2",
className,
@@ -49,8 +49,8 @@ export default function StackBadge({
className={cn(
"size-auto p-1 rounded-tl rounded-bl shadow-[inset_0px_-3px_4px_0px_rgba(0,0,0,0.06)] shadow-[inset_0px_3px_4px_0px_rgba(255,255,255,0.10)] outline outline-1 outline-offset-[-1px] flex justify-center items-center gap-2.5",
isSelected
? "bg-light-purple outline-primary"
: "bg-neutral-50 outline-neutral-300",
? "bg-interactive-secondary outline-primary"
: "bg-interactive-secondary outline-border",
)}
>
{asset ? (
@@ -62,7 +62,7 @@ export default function StackBadge({
<div
className={cn(
"justify-start text-xs font-medium font-['Inter']",
isSelected ? "text-primary" : "text-neutral-700",
isSelected ? "text-primary" : "text-t2",
)}
>
{stack}

View File

@@ -33,7 +33,7 @@ export const GroupedTabButton = ({
type="button"
onClick={() => onValueChange(option.value)}
className={cn(
"w-full flex items-center justify-center gap-1 px-[6px] py-1 h-6 text-body border transition-none outline-none whitespace-nowrap",
"w-full flex items-center justify-center gap-1 px-[6px] py-1 h-6 text-body border transition-none outline-none whitespace-nowrap !bg-interactive-secondary",
"hover:text-primary focus-visible:text-primary",
isActive &&
" text-primary shadow-[0px_3px_4px_0px_inset_rgba(0,0,0,0.04)]",

View File

@@ -44,7 +44,7 @@ export const ShortcutButton = ({
const variantClasses =
variant === "secondary"
? "bg-muted text-body-secondary"
: "bg-purple-medium dark:bg-interactive-secondary !text-primary-foreground";
: "bg-purple-medium dark:bg-interactive-secondary";
return (
<div className={`${baseClasses} ${variantClasses}`}>

View File

@@ -23,7 +23,6 @@ export function SheetHeader({
return (
<div className={cn("p-4 pb-0", className)}>
<h2 className="text-main">{title}</h2>
{/* check typography */}
<p
className={cn(

View File

@@ -12,7 +12,7 @@ interface ThemeContextType {
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useLocalStorage<Theme>("theme", "light");
const [theme, setTheme] = useLocalStorage<Theme>("theme", "system");
const [isDark, setIsDark] = useState(false);
useEffect(() => {

View File

@@ -122,8 +122,8 @@ html {
/* --input: oklch(0.92 0.004 286.32); */
--ring: oklch(0.705 0.015 286.067);
--chart-1: #9c5aff;
--chart-1: var(--primary);
/* --chart-2: #9c5aff; */
--chart-2: #a97eff;
--chart-3: #8268ff;
--chart-4: #7571ff;
@@ -196,11 +196,11 @@ html {
/* --border: oklch(0.274 0.006 286.033); */
--input: oklch(0.274 0.006 286.033);
--ring: oklch(0.442 0.017 285.786);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
/* --chart-1: oklch(0.488 0.243 264.376); */
/* --chart-2: oklch(0.696 0.17 162.48); */
/* --chart-3: oklch(0.769 0.188 70.08); */
/* --chart-4: oklch(0.627 0.265 303.9); */
/* --chart-5: oklch(0.645 0.246 16.439); */
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);

View File

@@ -74,7 +74,7 @@
0px 3px 15px -5px #6100f2 inset,
0px -2px 6px 0px #b27fff59 inset,
0px 3px 10px 2px #00000040,
0px 2px 0px 1px #661ecfb2 inset !important;
0px 2px 0px 1px #661ecfb2 inset !important
} */
.dark .btn-primary-dark {

View File

@@ -6,11 +6,10 @@ import { useNavigate } from "react-router";
import { toast } from "sonner";
import { z } from "zod";
import { CustomToaster } from "@/components/general/CustomToaster";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { IconButton } from "@/components/v2/buttons/IconButton";
import { Input } from "@/components/v2/inputs/Input";
import { useOrg } from "@/hooks/common/useOrg";
import { authClient, signIn } from "@/lib/auth-client";
import { cn } from "@/lib/utils";
import { getBackendErr } from "@/utils/genUtils";
import { OTPSignIn } from "./components/OTPSignIn";
@@ -117,17 +116,16 @@ export const SignIn = () => {
{!otpSent && (
<div className="space-y-6">
{/* Google Sign In Button */}
<Button
variant="auth"
<IconButton
variant="primary"
onClick={handleGoogleSignIn}
isLoading={googleLoading}
startIcon={
<FontAwesomeIcon icon={faGoogle} className="text-stone-400" />
}
className={height}
icon={<FontAwesomeIcon icon={faGoogle} />}
className={"w-full gap-2"}
>
{" "}
Continue with Google
</Button>
</IconButton>
{/* Divider */}
<div className="relative">
@@ -141,30 +139,28 @@ export const SignIn = () => {
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="text-base"
autoComplete="email"
/>
</div>
<div className="flex flex-col gap-2 w-full">
<Input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="text-base !w-full"
autoComplete="email"
/>
{/* Sign In Button */}
<Button
<IconButton
type="submit"
variant="auth"
variant="secondary"
isLoading={sendOtpLoading}
onClick={handleEmailSignIn}
className={cn(height)}
startIcon={<Mail size={14} className="text-stone-500" />}
className={"gap-2 w-full"}
icon={<Mail size={14} className="text-t4" />}
>
Continue with email
</Button>
Continue with Email
</IconButton>
</div>
</div>
)}

View File

@@ -86,12 +86,33 @@ export default function CustomerProductView() {
}
}, [entityIdParam]);
useEffect(() => {
const root = document.documentElement;
const previousTheme = root.classList.contains("dark") ? "dark" : "light";
// Force light mode
root.classList.remove("dark");
root.classList.add("light");
// Cleanup: restore previous theme when component unmounts
return () => {
root.classList.remove("light");
if (previousTheme === "dark") {
root.classList.add("dark");
}
};
}, []);
useEffect(() => {
if (!originalProduct) return;
const product = originalProduct;
console.log('[CPV] effect', { prodId: originalProduct.id, v: originalProduct.version, cusId: cusProduct?.id });
console.log("[CPV] effect", {
prodId: originalProduct.id,
v: originalProduct.version,
cusId: cusProduct?.id,
});
// Update initialProductRef BEFORE setProduct to ensure useAttachState
// effect has the correct baseline when it runs

View File

@@ -10,7 +10,7 @@ export const CodeSpan = ({
return (
<span
className={cn(
"bg-stone-200 font-mono text-t2 px-1 py-0.5 rounded-md",
"bg-interactive-secondary font-mono text-t2 px-1 py-0.5 rounded-md",
className,
)}
>

View File

@@ -1,4 +1,3 @@
import { productV2ToBasePrice } from "@autumn/shared";
import { PricingTableContainer } from "@/components/autumn/PricingTableContainer";
import { PlanTypeBadges } from "@/components/v2/badges/PlanTypeBadges";
import { Card, CardContent, CardHeader } from "@/components/v2/cards/Card";
@@ -78,7 +77,7 @@ export const OnboardingPreview = ({
return (
<Card
className={cn(
"min-w-[28rem] max-w-xl mx-4 bg-card border-border border-[0.5px] p-4",
"min-w-[28rem] max-w-xl mx-4 bg-card shadow-none p-4",
showDummyFeature && !showFeatures && "pb-0",
currentStep === 1 && "!gap-0",
)}

View File

@@ -100,16 +100,16 @@ export default function OnboardingContent() {
{/* Standard layout for all steps - NO PROP DRILLING! */}
<div
className={cn(
"relative w-full h-full flex bg-gray-medium [scrollbar-gutter:stable]",
"relative w-full h-full flex bg-background [scrollbar-gutter:stable]",
step === OnboardingStep.Integration
? "overflow-y-auto bg-t13"
? "overflow-y-auto"
: "overflow-y-hidden",
)}
>
{/* Exit button */}
<ExitButton />
<div className="w-4/5 flex items-center justify-center relative ">
<div className="w-4/5 flex items-center justify-center relative">
{/* Components access Zustand directly - no props! */}
{step === OnboardingStep.Integration ? (
<div className="w-full h-full flex items-start justify-center py-8 pl-10">
@@ -139,15 +139,15 @@ export default function OnboardingContent() {
)}
>
<div className="rounded-lg h-full flex flex-col p-1 gap-[0.625rem] overflow-x-hidden">
<div className="bg-card border-base border overflow-x-hidden rounded-[12px] mt-1 p-4 shrink-0">
<div className=" overflow-x-hidden rounded-[12px] mt-1 p-4 shrink-0">
<StepHeader />
</div>
{step !== OnboardingStep.Integration && (
<SheetContainer className="bg-card border-base border overflow-x-hidden rounded-[12px] p-0 flex-1 pr-0">
<SheetContainer className="bg-card border overflow-x-hidden rounded-[12px] p-0 flex-1 pr-0">
<OnboardingStepRenderer />
</SheetContainer>
)}
<div className="bg-card border-base border rounded-[12px] flex flex-col p-4 shrink-0">
<div className=" rounded-[12px] flex flex-col p-4 shrink-0">
<div className="flex items-center justify-center">
<OnboardingSteps />
</div>

View File

@@ -5,8 +5,8 @@ import { useFeatureStore } from "@/hooks/stores/useFeatureStore";
import { useProductStore } from "@/hooks/stores/useProductStore";
import { useSheetStore } from "@/hooks/stores/useSheetStore";
import { getItemId } from "@/utils/product/productItemUtils";
import { EditPlanSheet } from "@/views/products/plan/components/EditPlanSheet";
import { ProductItemContext } from "@/views/products/product/product-item/ProductItemContext";
import { EditPlanSheet } from "../../products/plan/components/EditPlanSheet";
import { EditPlanFeatureSheet } from "../../products/plan/components/edit-plan-feature/EditPlanFeatureSheet";
import { NewFeatureSheet } from "../../products/plan/components/new-feature/NewFeatureSheet";
import { SelectFeatureSheet } from "../../products/plan/components/SelectFeatureSheet";

View File

@@ -38,7 +38,7 @@ export const OnboardingSteps = ({ className }: OnboardingStepsProps) => {
className,
)}
>
<div className="self-stretch w-full h-6 px-2.5 py-3 bg-white rounded-lg shadow-[inset_0px_-3px_4px_0px_rgba(0,0,0,0.04)] outline-1 outline-offset-[-1px] outline-border inline-flex justify-start items-center gap-1">
<div className="self-stretch w-full h-6 px-2.5 py-3 bg-interactive-secondary rounded-lg shadow-[inset_0px_-3px_4px_0px_rgba(0,0,0,0.04)] outline-1 outline-offset-[-1px] outline-border inline-flex justify-start items-center gap-1">
{Array.from({ length: totalSteps }).map((_, index) => {
const isCompleted = index < currentStep;
return (

View File

@@ -3,6 +3,7 @@ import { Input } from "@/components/v2/inputs/Input";
import { SheetSection } from "@/components/v2/sheets/InlineSheet";
import { useAutoSlug } from "@/hooks/common/useAutoSlug";
import { useProductStore } from "@/hooks/stores/useProductStore";
import { PlanTypeSection } from "@/views/products/plan/components/edit-plan-details/PlanTypeSection";
import { BasePriceSection } from "../../products/plan/components/edit-plan-details/BasePriceSection";
export const PlanDetailsStep = () => {
@@ -35,7 +36,7 @@ export const PlanDetailsStep = () => {
className="mb-1"
/>
<span className="text-form-label block">
<span className="text-t4 text-sm block">
This name will be displayed on Stripe's checkout page
</span>
</div>
@@ -47,7 +48,7 @@ export const PlanDetailsStep = () => {
onChange={(e) => setTarget(e.target.value)}
className="mb-1"
/>
<span className="text-form-label block">
<span className="text-t4 text-sm block">
Used to refer to this plan when using Autumn's APIs or SDKs
</span>
</div>
@@ -69,7 +70,7 @@ export const PlanDetailsStep = () => {
</div>
</div>
</SheetSection>
<PlanTypeSection />
<BasePriceSection withSeparator={false} />
</>
);

View File

@@ -31,7 +31,8 @@ export const StepHeader = () => {
return (
<SheetHeader
title={`Step ${stepNum}: ${config.title}`}
// title={`Step ${stepNum}: ${config.title}`}
title={`${config.title}`}
description={config.description}
noSeparator={true}
className="p-0 sticky"

View File

@@ -26,10 +26,10 @@ export const MCPSection = () => {
};
return (
<div className="px-3 py-3.5 bg-gray-50 rounded-lg outline-[1.50px] outline-offset-[-1.50px] outline-violet-600 flex flex-col items-start gap-4">
<div className="px-3 py-3.5 bg-interactive-secondary rounded-lg outline-[1.50px] outline-offset-[-1.50px] outline-violet-600 flex flex-col items-start gap-4">
<div className="inline-flex justify-start items-center gap-1.5">
<InfoIcon fill="var(--primary)" size={16} weight="fill" />
<div className="justify-start text-zinc-800 text-xs font-semibold font-['Inter']">
<div className="justify-start text-t2 text-xs font-semibold font-['Inter']">
Install our MCP and prompt the model to use the `autumn-docs` MCP to
integrate Autumn
</div>

View File

@@ -27,11 +27,11 @@ const NextStepCard = ({ title, description, href }: NextStepCardProps) => {
<Card
className={cn(
"group gap-1 my-0 cursor-pointer",
"hover:!bg-hover-primary hover:border-primary",
"hover:!bg-interactive-secondary-hover hover:border-primary",
)}
>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-sub text-t9">{title}</CardTitle>
<CardTitle className="text-sub text-t2">{title}</CardTitle>
<IconButton
icon={<ArrowSquareOutIcon className="ml-auto" />}
variant="skeleton"
@@ -53,7 +53,7 @@ export const NextStepsSection = () => {
<SectionHeader
stepNumber={7}
title={<span>Next Steps</span>}
description="Congrats on setting up Autumn! The next steps are to learn how to use Autumn to check if a user has access to features in your application, and track usage for those features. Learn how to do so here."
description="Congrats on setting up Autumn! The next steps are to learn how to use Autumn to check if a user has access to features in your application, and track usage for those features. Learn how to do so here."
/>
<div className="pl-[32px] flex flex-col gap-6">

View File

@@ -1,3 +1,4 @@
import { productV2ToFrontendProduct } from "@autumn/shared";
import { useEffect, useRef } from "react";
import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
import { useProductStore } from "@/hooks/stores/useProductStore";
@@ -45,9 +46,14 @@ export const useOnboardingProductSync = () => {
}
}
// Convert ProductV2 to FrontendProduct before setting in store
const frontendProduct = productV2ToFrontendProduct({
product: selectedProduct,
});
// Set both product and baseProduct
setBaseProduct(selectedProduct);
setProduct(selectedProduct);
setBaseProduct(frontendProduct);
setProduct(frontendProduct);
if (!hasInitialized.current) {
hasInitialized.current = true;

View File

@@ -4,7 +4,9 @@ import {
FeatureUsageType,
isPriceItem,
productV2ToBasePrice,
productV2ToPlanType,
} from "@autumn/shared";
import { useHasChanges } from "@/hooks/stores/useProductStore";
import { OnboardingStep } from "../utils/onboardingUtils";
import { useOnboarding3QueryState } from "./useOnboarding3QueryState";
@@ -12,6 +14,7 @@ export const useOnboardingSteps = () => {
// Use query state instead of local state
const { queryStates } = useOnboarding3QueryState();
const step = queryStates.step;
const hasChanges = useHasChanges();
// Step validation
const validateStep = (
@@ -32,6 +35,9 @@ export const useOnboardingSteps = () => {
// Base price validation - safely check if product has items
if (!product.items) return false;
const planType = productV2ToPlanType({ product });
if (!planType) return false;
const basePrice = productV2ToBasePrice({
product: product as unknown as ProductV2,
});
@@ -59,7 +65,7 @@ export const useOnboardingSteps = () => {
case OnboardingStep.FeatureConfiguration:
return true;
case OnboardingStep.Playground:
return true;
return !hasChanges;
case OnboardingStep.Integration:
return true;
default:

View File

@@ -61,22 +61,22 @@ export const getNextStep = (
// Step configuration for headers and descriptions
export const stepConfig = {
[OnboardingStep.PlanDetails]: {
title: "Create a plan",
title: "Create your first Plan",
description:
"Plans are the pricing tiers your application offers. You can create your free tiers and your paid tiers too.",
"Create the first plan your app offers. You can create both free and paid tiers.",
},
[OnboardingStep.FeatureCreation]: {
title: "Create a feature",
title: "Create a Feature",
description:
"Features are the benefits customers get access to when using this plan. You can create a feature for things in your app you want to limit, track or bill for.",
"Features are what customers get access to with this plan. Create features for things you want to limit, track or bill for.",
},
[OnboardingStep.FeatureConfiguration]: {
title: "Define feature limits or billing",
title: "Configure Plan Limits and Pricing",
description:
"Features can be included as part of this plan, or billed for based on their usage.",
"Set a limit for how much the feature can be used, or charge a usage-based price for it.",
},
[OnboardingStep.Playground]: {
title: "Finish your setup",
title: "Add more Features and Plans",
description:
"Take your time setting up plans and features. Revisit anytime. Use Preview Mode to test features, limits and upgrade/downgrade flows.",
},

View File

@@ -302,8 +302,8 @@ export const BasePriceSection = ({
infoBox: "text-sm py-2",
}}
>
You can add usage-based prices (eg, per seat, per credit) when you
link a feature to this plan.
You can charge based on the quantity of a feature (eg per seat,
per credit) when you link features to this plan.
</InfoBox>
)}
</div>