feat: 🎸 lots of stuff

This commit is contained in:
amianthus
2025-10-17 11:28:53 +01:00
parent 00966ec348
commit 07ffa08881
34 changed files with 260 additions and 142 deletions

View File

@@ -6,7 +6,7 @@
- When using db schemas in Drizzle, import them from '@autumn/shared', and don't do schemas.
# Linting and Codebase rules
- You can access the biome linter by running `npx biome check <folder or file path>`. Always specify a folder path, as the codebase is quite large and you will get out of scope errors that you are not burdened to correct. If you would like to let biome automatically fix as much as it can, use `npx biome check --write <folder or file path>`
- You can access the biome linter by running `bunx biome check <folder or file path>`. Always specify a folder path, as the codebase is quite large and you will get out of scope errors that you are not burdened to correct. If you would like to let biome automatically fix as much as it can, use `bunx biome check --write <folder or file path>`
- Note, biome does not perform typechecking. In which case you need to, you may run `tsc --noEmit --skipLibCheck <folder or file path>`

View File

@@ -21,9 +21,14 @@ const badgeVariants = cva(
export interface PlanTypeBadgeProps extends VariantProps<typeof badgeVariants> {
className?: string;
iconOnly?: boolean;
}
export const PlanTypeBadge = ({ variant, className }: PlanTypeBadgeProps) => {
export const PlanTypeBadge = ({
variant,
className,
iconOnly,
}: PlanTypeBadgeProps) => {
const getIcon = () => {
switch (variant) {
case "default":
@@ -53,7 +58,7 @@ export const PlanTypeBadge = ({ variant, className }: PlanTypeBadgeProps) => {
return (
<div className={cn(badgeVariants({ variant }), className)}>
{getIcon()}
<span>{getLabel()}</span>
{!iconOnly && <span>{getLabel()}</span>}
</div>
);
};

View File

@@ -4,14 +4,24 @@ import { PlanTypeBadge } from "./PlanTypeBadge";
interface PlanTypeBadgesProps {
product: ProductV2;
className?: string;
iconOnly?: boolean;
}
export const PlanTypeBadges = ({ product, className }: PlanTypeBadgesProps) => {
export const PlanTypeBadges = ({
product,
className,
iconOnly = false,
}: PlanTypeBadgesProps) => {
const badges = [];
if (product.is_default) {
badges.push(
<PlanTypeBadge key="default" variant="default" className={className} />,
<PlanTypeBadge
key="default"
variant="default"
className={className}
iconOnly={iconOnly}
/>,
);
}
@@ -21,13 +31,19 @@ export const PlanTypeBadges = ({ product, className }: PlanTypeBadgesProps) => {
key="freeTrial"
variant="freeTrial"
className={className}
iconOnly={iconOnly}
/>,
);
}
if (product.is_add_on) {
badges.push(
<PlanTypeBadge key="addon" variant="addon" className={className} />,
<PlanTypeBadge
key="addon"
variant="addon"
className={className}
iconOnly={iconOnly}
/>,
);
}

View File

@@ -39,9 +39,9 @@ export const GroupedTabButton = ({
"bg-light-purple text-primary shadow-[0px_3px_4px_0px_inset_rgba(0,0,0,0.04)]",
!isActive &&
"bg-white shadow-[0px_-3px_4px_0px_inset_rgba(0,0,0,0.04)]",
isFirst && "rounded-l-md border-l",
isFirst && "rounded-l-lg border-l",
!isFirst && "border-l-0",
isLast && "rounded-r-md",
isLast && "rounded-r-lg",
)}
>
{isTwoTab && isFirst && option.icon && (

View File

@@ -9,6 +9,7 @@ import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
import { useFeatureStore } from "@/hooks/stores/useFeatureStore";
import { useProductStore } from "@/hooks/stores/useProductStore";
import { useIsEditingPlan, useSheetStore } from "@/hooks/stores/useSheetStore";
import { cn } from "@/lib/utils";
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
import { PlanCardToolbar } from "../products/plan/components/plan-card/PlanCardToolbar";
import { PlanFeatureList } from "../products/plan/components/plan-card/PlanFeatureList";
@@ -17,6 +18,7 @@ import { useOnboarding3QueryState } from "./hooks/useOnboarding3QueryState";
import { useOnboardingStore } from "./store/useOnboardingStore";
import { getStepNumber } from "./utils/onboardingUtils";
const MAX_PLAN_NAME_LENGTH = 20;
interface OnboardingPreviewProps {
setConnectStripeOpen?: (open: boolean) => void;
}
@@ -79,36 +81,55 @@ export const OnboardingPreview = ({
}
return (
<Card className="min-w-[28rem] max-w-xl mx-4 bg-card border-border border-[0.5px] p-4">
<CardHeader className="gap-0 px-0">
<div className="flex flex-row items-center justify-between w-full">
<div className="flex flex-row items-center gap-2 min-w-0 flex-1">
{showBasicInfo && product?.name ? (
<span className="text-main-sec truncate">{product.name}</span>
) : (
<span className="text-main-sec !text-t4 truncate">
Name your product
</span>
)}
<Card
className={cn(
"min-w-[28rem] max-w-xl mx-4 bg-card border-border border-[0.5px] p-4",
showDummyFeature && !showFeatures && "pb-0",
)}
>
<CardHeader className="gap-0 px-0 relative">
{/* Absolutely positioned toolbar - CANNOT MOVE */}
{showToolbar && (
<div className="absolute top-0 right-0 z-10">
<PlanCardToolbar
onEdit={handleEdit}
onDeleteSuccess={handleDeletePlanSuccess || undefined}
deleteDisabled={allProducts?.length === 1}
deleteTooltip={
allProducts?.length === 1
? "At least 1+ plan is required."
: undefined
}
/>
</div>
)}
{playgroundMode === "edit" && product && (
<PlanTypeBadges product={product} />
{/* Left content with padding to avoid toolbar */}
<div className={cn("flex items-center gap-2", showToolbar && "pr-20")}>
<div className="min-w-0 overflow-hidden">
{showBasicInfo && product?.name ? (
<div className="text-main-sec min-w-0 max-w-[50%]">
<span className="truncate max-w-full">
{product.name.length > MAX_PLAN_NAME_LENGTH
? `${product.name.slice(0, MAX_PLAN_NAME_LENGTH)}...`
: product.name}
</span>
</div>
) : (
<div className="text-main-sec !text-t4 truncate">
Name your plan
</div>
)}
</div>
<div className="flex flex-row items-center gap-1">
{showToolbar && (
<PlanCardToolbar
onEdit={handleEdit}
onDeleteSuccess={handleDeletePlanSuccess || undefined}
deleteDisabled={allProducts?.length === 1}
deleteTooltip={
allProducts?.length === 1
? "At least 1+ product is required."
: undefined
}
{playgroundMode === "edit" && product && (
<div className="flex-shrink-0">
<PlanTypeBadges
product={product}
iconOnly={product.name.length > MAX_PLAN_NAME_LENGTH - 10}
/>
)}
</div>
</div>
)}
</div>
{showPricing && (
@@ -134,7 +155,7 @@ export const OnboardingPreview = ({
</CardHeader>
{showDummyFeature && feature && (
<>
<Separator className="my-2" />
<Separator className="" />
<DummyFeatureRow feature={feature} />
</>
)}

View File

@@ -34,6 +34,7 @@ export const OnboardingStepRenderer = () => {
const sheetType = useSheetStore((s) => s.type);
const itemId = useSheetStore((s) => s.itemId);
const [trackResponse, setTrackResponse] = useState<any>(null);
const [checkResponse, setCheckResponse] = useState<any>(null);
const [lastUsedFeatureId, setLastUsedFeatureId] = useState<
string | undefined
>(undefined);
@@ -183,10 +184,12 @@ export const OnboardingStepRenderer = () => {
<>
<AvailableFeatures
onTrackSuccess={setTrackResponse}
onCheckSuccess={setCheckResponse}
onFeatureUsed={setLastUsedFeatureId}
/>
<QuickStartCodeGroup
trackResponse={trackResponse}
checkResponse={checkResponse}
featureId={lastUsedFeatureId}
/>
</>

View File

@@ -23,7 +23,7 @@ export const PlanDetailsStep = () => {
return (
<>
<SheetSection title="Product Details">
<SheetSection title="Plan Details">
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div>
@@ -36,7 +36,7 @@ export const PlanDetailsStep = () => {
/>
<span className="text-form-label block">
The display name of the product that will show up on your
The display name of the plan that will show up on your
checkout page
</span>
</div>
@@ -49,8 +49,7 @@ export const PlanDetailsStep = () => {
className="mb-1"
/>
<span className="text-form-label block">
A fixed price to charge for the product. Uncheck this section if
the product is free or a variable price.
Used to refer to this plan when using Autumn's APIs or SDKs
</span>
</div>
{/* {step === OnboardingStep.Playground && product && (

View File

@@ -41,7 +41,7 @@ export const ConnectStripeSection = () => {
title="Connect Stripe"
description={
<span>
Stripe is required to checkout and add your products to customers.
Stripe is required to checkout and add your plans to customers.
Grab your API key{" "}
<a
href="https://dashboard.stripe.com/apikeys"

View File

@@ -68,12 +68,14 @@ const FeatureTestRow = ({
export const AvailableFeatures = ({
onTrackSuccess,
onCheckSuccess,
onFeatureUsed,
}: {
onTrackSuccess?: (response: any) => void;
onCheckSuccess?: (response: any) => void;
onFeatureUsed?: (featureId: string) => void;
}) => {
const { customer, track, refetch } = useCustomer();
const { customer, track, refetch, check } = useCustomer();
const { features } = useFeaturesQuery();
return (
@@ -101,6 +103,16 @@ export const AvailableFeatures = ({
handleSend={async (value) => {
const featureId = customer?.features[x].id;
// Check the feature access
const { data: checkResponse, error: checkError } =
await check({
featureId: featureId,
requiredBalance: value,
});
if (!checkError && checkResponse && onCheckSuccess) {
onCheckSuccess(checkResponse);
}
// Notify parent which feature was used
if (onFeatureUsed && featureId !== undefined) {
onFeatureUsed(featureId);
@@ -123,8 +135,8 @@ export const AvailableFeatures = ({
))
) : (
<span className="text-sm text-muted-foreground">
Your current product doesn't have any features. Try purchasing a
product in the preview first.
Your current plan doesn't have any features. Try purchasing a
plan in the preview first.
</span>
)}
</div>

View File

@@ -50,30 +50,43 @@ export const PlaygroundToolbar = () => {
},
]}
/>
<div className="flex gap-2 items-center">
<Select
value={product?.id}
onValueChange={(id) => handlePlanSelect?.(id)}
>
<SelectTrigger className="!h-6 text-body px-2 py-1 min-w-0 max-w-[120px] overflow-hidden text-ellipsis whitespace-nowrap">
<SelectValue placeholder="Select product" className="truncate" />
</SelectTrigger>
<SelectContent>
{products?.map((prod) => (
<SelectItem key={prod.id} value={prod.id} className="text-body">
<span className="truncate block max-w-[100px]">
{prod.name}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<CreatePlanDialog
onSuccess={onCreatePlanSuccess || undefined}
size="sm"
buttonClassName="!h-6 !px-2 text-body"
/>
</div>
{playgroundMode === "edit" && (
<div className="flex gap-2 items-center">
{products.filter((p) => !p.archived).length > 1 && (
<Select
value={product?.id}
onValueChange={(id) => handlePlanSelect?.(id)}
>
<SelectTrigger className="!h-6 text-body px-2 py-1 min-w-0 max-w-[120px] overflow-hidden text-ellipsis whitespace-nowrap">
<SelectValue
placeholder="Select plan"
className="truncate"
/>
</SelectTrigger>
<SelectContent>
{products
.filter((p) => !p.archived)
.map((prod) => (
<SelectItem
key={prod.id}
value={prod.id}
className="text-body"
>
<span className="truncate block max-w-[100px]">
{prod.name}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
)}
<CreatePlanDialog
onSuccess={onCreatePlanSuccess || undefined}
size="sm"
buttonClassName="!h-6 !px-2 text-body"
/>
</div>
)}
</div>
);
};

View File

@@ -1,5 +1,6 @@
import type { ProductItem } from "@autumn/shared";
import type { TrackResult } from "autumn-js";
import type { CheckResult, TrackResult } from "autumn-js";
import { useCustomer } from "autumn-js/react";
import { useEffect, useState } from "react";
import {
CodeGroup,
@@ -10,7 +11,8 @@ import {
CodeGroupTab,
} from "@/components/v2/CodeGroup";
import { SheetSection } from "@/components/v2/sheets/InlineSheet";
import { useProductContext } from "@/views/products/product/ProductContext";
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
import { useProductStore } from "@/hooks/stores/useProductStore";
import { getCodeSnippets } from "../../utils/completionStepCode";
type CodeLanguage = "react" | "nodejs" | "response";
@@ -19,28 +21,34 @@ const CodeSnippetSection = ({
title,
snippets,
trackResponse,
checkResponse,
}: {
title: string;
snippets: { react: string; nodejs: string; response: string };
trackResponse?: TrackResult;
checkResponse?: CheckResult;
}) => {
const [activeLanguage, setActiveLanguage] = useState<CodeLanguage>("react");
// Auto-switch to response tab when trackResponse is available (for track section only)
// Auto-switch to response tab when responses are available
useEffect(() => {
if (trackResponse && title === "Track usage") {
setActiveLanguage("response");
}
}, [trackResponse, title]);
if (checkResponse && title === "Check feature access") {
setActiveLanguage("response");
}
}, [trackResponse, checkResponse, title]);
const getCodeForTab = () => {
// Use dynamic trackResponse for track section response tab
if (
activeLanguage === "response" &&
title === "Track usage" &&
trackResponse
) {
return JSON.stringify(trackResponse, null, 2);
// Use dynamic responses for response tabs
if (activeLanguage === "response") {
if (title === "Track usage" && trackResponse) {
return JSON.stringify(trackResponse, null, 2);
}
if (title === "Check feature access" && checkResponse) {
return JSON.stringify(checkResponse, null, 2);
}
}
return snippets[activeLanguage];
};
@@ -70,7 +78,35 @@ const CodeSnippetSection = ({
<CodeGroupCode language="json">
{title === "Track usage" && trackResponse
? JSON.stringify(trackResponse, null, 2)
: snippets.response}
: title === "Check feature access" && checkResponse
? JSON.stringify(checkResponse, null, 2)
: snippets.response}
</CodeGroupCode>
</CodeGroupContent>
</CodeGroup>
</div>
);
};
const CustomerSection = () => {
const { customer } = useCustomer();
const [activeTab, setActiveTab] = useState("customer");
return (
<div className="space-y-2">
<h3 className="text-sub">Customer</h3>
<CodeGroup value={activeTab} onValueChange={setActiveTab}>
<CodeGroupList>
<CodeGroupTab value="customer">Customer Response</CodeGroupTab>
<CodeGroupCopyButton
onCopy={() =>
navigator.clipboard.writeText(JSON.stringify(customer, null, 2))
}
/>
</CodeGroupList>
<CodeGroupContent value="customer">
<CodeGroupCode language="json">
{JSON.stringify(customer, null, 2)}
</CodeGroupCode>
</CodeGroupContent>
</CodeGroup>
@@ -80,12 +116,15 @@ const CodeSnippetSection = ({
export const QuickStartCodeGroup = ({
trackResponse,
checkResponse,
featureId: usedFeatureId,
}: {
trackResponse?: TrackResult;
checkResponse?: CheckResult;
featureId?: string;
}) => {
const { product } = useProductContext();
const { product } = useProductStore();
const { features } = useFeaturesQuery();
// Use the feature that was actually used (if available), otherwise fallback to first feature
const firstFeatureItem = product?.items?.find(
@@ -94,8 +133,11 @@ export const QuickStartCodeGroup = ({
const featureId = usedFeatureId || firstFeatureItem?.feature_id || undefined;
const productId = product?.id || undefined;
// Get the actual feature name from features list
const featureName = features.find((f) => f.id === featureId)?.name;
// Generate snippets with actual IDs from onboarding
const snippets = getCodeSnippets(featureId, productId);
const snippets = getCodeSnippets(featureId, productId, featureName);
return (
<SheetSection>
@@ -103,6 +145,7 @@ export const QuickStartCodeGroup = ({
<CodeSnippetSection
title="Check feature access"
snippets={snippets.allowed}
checkResponse={checkResponse}
/>
<CodeSnippetSection
title="Track usage"
@@ -113,6 +156,7 @@ export const QuickStartCodeGroup = ({
title="Create checkout session"
snippets={snippets.checkout}
/>
<CustomerSection />
</div>
</SheetSection>
);

View File

@@ -38,7 +38,7 @@ export const usePlanDetailsActions = () => {
product: product as ProductV2,
onSuccess: async () => {},
});
toast.success("Product updated successfully");
toast.success("Plan updated successfully");
} else {
// Create new product
newProduct = await createProduct(product, axiosInstance);

View File

@@ -1,6 +1,11 @@
export const getCodeSnippets = (featureId?: string, productId?: string) => {
export const getCodeSnippets = (
featureId?: string,
productId?: string,
featureName?: string,
) => {
const actualFeatureId = featureId || "your_feature_id";
const actualProductId = productId || "your_product_id";
const actualFeatureName = featureName || "Your Feature";
return {
allowed: {
@@ -27,7 +32,7 @@ const allowed = await autumn.check({
"allowed": true,
"feature": {
"id": "${actualFeatureId}",
"name": "Your Feature",
"name": "${actualFeatureName}",
"type": "limit"
}
}`,
@@ -73,7 +78,7 @@ console.log(session.checkout_url);`,
"customer_id": "cust_123",
"product": {
"id": "${actualProductId}",
"name": "Your Product",
"name": "Your Plan",
"items": [
{
"type": "price",

View File

@@ -61,19 +61,19 @@ export const getNextStep = (
// Step configuration for headers and descriptions
export const stepConfig = {
[OnboardingStep.PlanDetails]: {
title: "Create a product",
title: "Create a plan",
description:
"Products are the pricing tiers your application offers. You can create your free tiers and your paid tiers too.",
"Plans are the pricing tiers your application offers. You can create your free tiers and your paid tiers too.",
},
[OnboardingStep.FeatureCreation]: {
title: "Create a feature",
description:
"Create and add the first feature that customers on this plan get access to. One feature for each part of your app you want to gate based on pricing.",
"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.",
},
[OnboardingStep.FeatureConfiguration]: {
title: "Define feature limits or billing",
description:
"Features can be included as part of this product, or billed for based on their usage.",
"Features can be included as part of this plan, or billed for based on their usage.",
},
[OnboardingStep.Playground]: {
title: "Finish your setup",
@@ -273,7 +273,7 @@ export const createProduct = async (
// created: true,
// latestId: createdProduct.id,
// };
toast.success(`Product "${product?.name}" created successfully!`);
toast.success(`Plan "${product?.name}" created successfully!`);
// if (!productCreatedRef.current.created) {
// // First time creating the product

View File

@@ -28,7 +28,7 @@ export const ProductConfig = ({
<div className="flex flex-col flex-1">
<FieldLabel className="mb-1">Name</FieldLabel>
<Input
placeholder="eg. Starter Product"
placeholder="eg. Starter Plan"
value={product.name}
onChange={(e) => {
setSource(e.target.value);
@@ -40,7 +40,7 @@ export const ProductConfig = ({
<div className="flex items-center">
<Input
autoFocus={idEdit}
placeholder="eg. Product ID"
placeholder="eg. Plan ID"
// disabled={!idEdit}
className="disabled:bg-transparent disabled:border-none disabled:shadow-none"
value={product.id}

View File

@@ -133,14 +133,14 @@ export const DeletePlanDialog = ({
const getDeleteMessage = () => {
if (product.archived) {
return `Are you sure you want to unarchive ${product.name}? This will make it visible in your list of products.`;
return `Are you sure you want to unarchive ${product.name}? This will make it visible in your list of plans.`;
}
// \n\nNote: If there are multiple versions, this will unarchive all versions at once.
const isMultipleVersions = productInfo?.numVersion > 1;
const versionText = deleteAllVersions ? "product" : "version";
const productText = isMultipleVersions ? versionText : "product";
const versionText = deleteAllVersions ? "plan" : "version";
const productText = isMultipleVersions ? versionText : "plan";
const messageTemplates = {
withCustomers: {
@@ -151,9 +151,9 @@ export const DeletePlanDialog = ({
otherCount: number,
productText: string,
) =>
`${customerName} and ${otherCount} other customer${otherCount > 1 ? "s" : ""} are on this ${productText}. Are you sure you want to archive this product?`,
`${customerName} and ${otherCount} other customer${otherCount > 1 ? "s" : ""} are on this ${productText}. Are you sure you want to archive this plan?`,
fallback: (productText: string) =>
`There are customers on this ${productText}. Deleting this ${productText} will remove it from their accounts. Are you sure you want to continue? You can also archive the product instead.`,
`There are customers on this ${productText}. Deleting this ${productText} will remove it from their accounts. Are you sure you want to continue? You can also archive the plan instead.`,
},
withoutCustomers: (productText: string) =>
`Are you sure you want to delete this ${productText}? This action cannot be undone.`,
@@ -224,7 +224,7 @@ export const DeletePlanDialog = ({
</SelectTrigger>
<SelectContent>
<SelectItem value="latest">Delete latest version</SelectItem>
<SelectItem value="all">Archive product</SelectItem>
<SelectItem value="all">Archive plan</SelectItem>
</SelectContent>
</Select>
)}

View File

@@ -110,7 +110,7 @@ export const EditPlanHeader = () => {
className="p-0"
items={[
{
name: "Products",
name: "Plans",
href: "/products?tab=products",
},
{

View File

@@ -14,7 +14,7 @@ export function EditPlanSheet({ isOnboarding }: { isOnboarding?: boolean }) {
<>
{!isOnboarding && (
<SheetHeader
title={`Configure ${product.name ? product.name : "your new product"}`}
title={`Configure ${product.name ? product.name : "your new plan"}`}
description="Configure how this feature is used in your app"
/>
)}

View File

@@ -46,7 +46,7 @@ export const SaveChangesBar = ({
const handleSaveClicked = async () => {
if (!isOnboarding && isLoading) {
toast.error("Product counts are loading");
toast.error("Plan counts are loading");
return;
}

View File

@@ -62,7 +62,7 @@ export function SelectFeatureSheet({
{!isOnboarding && (
<SheetHeader
title="Select Feature"
description="Choose a feature to add to this product"
description="Choose a feature to add to this plan"
/>
)}

View File

@@ -15,8 +15,8 @@ export const AdditionalOptions = ({
<div className="space-y-4">
<AreaCheckbox
title="Enable by default"
description="This product will be enabled by default for all new users,
typically used for your free product"
description="This plan will be enabled by default for all new users,
typically used for your free plan"
checked={product.is_default}
disabled={product.is_add_on}
onCheckedChange={(checked) =>
@@ -25,8 +25,8 @@ export const AdditionalOptions = ({
/>
<AreaCheckbox
title="Add On"
description="This product is an add-on that can be bought together with your
base products (eg, top ups)"
description="This plan is an add-on that can be bought together with your
base plans (eg, top ups)"
checked={product.is_add_on}
disabled={product.is_default}
onCheckedChange={(checked) =>

View File

@@ -8,7 +8,7 @@ export const MainDetailsSection = () => {
const setProduct = useProductStore((s) => s.setProduct);
return (
<SheetSection title="Product Details">
<SheetSection title="Plan Details">
<div className="space-y-4">
<div className="grid grid-cols-2 gap-2">
<div>

View File

@@ -81,10 +81,10 @@ export function AdvancedSettings() {
description="Additional configuration options for this feature"
>
<div className="space-y-6 pt-2 pb-10 [>&_.advanced-input-width]:w-xs">
{/* Reset existing usage when product is enabled */}
{/* Reset existing usage when plan is enabled */}
<AreaCheckbox
title="Reset existing usage when product is enabled"
description="When coming from another product, this will reset the customer's feature usage to 0."
title="Reset existing usage when plan is enabled"
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={

View File

@@ -43,7 +43,7 @@ export function IncludedUsage() {
plural: true,
})}
' that this customer is granted before{" "}
{isFeaturePrice ? "hitting the limit" : "being charged"}.
{!isFeaturePrice ? "hitting the limit" : "being charged"}.
</div>
<div className="flex items-center gap-2">
<Input

View File

@@ -60,7 +60,7 @@ export const PlanCardToolbar = ({
<IconButton
icon={<PencilSimpleIcon />}
onClick={onEdit}
aria-label="Edit product"
aria-label="Edit plan"
variant="muted"
disabled={editDisabled}
iconOrientation="center"
@@ -75,7 +75,7 @@ export const PlanCardToolbar = ({
<IconButton
icon={<TrashIcon />}
onClick={() => setDeleteOpen(true)}
aria-label="Delete product"
aria-label="Delete plan"
variant="muted"
iconOrientation="center"
disabled={deleteDisabled}

View File

@@ -2,7 +2,7 @@ import { useCallback } from "react";
import { useBlocker } from "@/views/products/product/hooks/useBlocker";
const DEFAULT_MESSAGE =
"Are you sure you want to leave without updating the product? Your changes will be lost.";
"Are you sure you want to leave without updating the plan? Your changes will be lost.";
export const useProductChangedAlert = ({
hasChanges,

View File

@@ -91,7 +91,7 @@ export default function ProductSidebar() {
}
disabledReason={
isOneOffProduct(product.items, product.is_add_on)
? "Can't add a free trial to an a one time product"
? "Can't add a free trial to an a one time plan"
: undefined
}
>
@@ -100,7 +100,7 @@ export default function ProductSidebar() {
<FreeTrialView product={product} />
) : (
<span className="text-t3">
Add a free trial to this product.
Add a free trial to this plan.
</span>
)}
</div>

View File

@@ -35,7 +35,7 @@ function ProductView() {
if (error) {
return (
<ErrorScreen returnUrl="/products">
{error ? error.message : `Product ${product_id} not found`}
{error ? error.message : `Plan ${product_id} not found`}
</ErrorScreen>
);
}

View File

@@ -22,7 +22,7 @@ export default function ProductViewBreadcrumbs() {
onClick={() => navigateTo("/products", navigate, env)}
className="cursor-pointer"
>
Products
Plans
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem className="cursor-pointer">

View File

@@ -26,7 +26,7 @@ export const NavigationBlockerModal: React.FC<NavigationBlockerModalProps> = ({
<DialogHeader>
<DialogTitle>Unsaved Changes</DialogTitle>
<DialogDescription>
Are you sure you want to leave without updating the product? Your
Are you sure you want to leave without updating the plan? Your
changes will be lost.
</DialogDescription>
</DialogHeader>

View File

@@ -49,7 +49,7 @@ const ToggleProductDialog = ({
const getTitle = () => {
if (toggleKey === "is_default") {
return value
? `Make ${product.name} a default product`
? `Make ${product.name} a default plan`
: `Remove default from ${product.name}`;
} else {
return value
@@ -119,14 +119,14 @@ export const ToggleDefaultProduct = ({
await ProductService.updateProduct(axiosInstance, product.id, data);
// mutate();
setOpen(false);
toast.success("Successfully updated product");
toast.success("Successfully updated plan");
} catch (error) {
setProduct({
...product,
[toggleKey]: !value,
});
toast.error(getBackendErr(error, "Failed to update product"));
toast.error(getBackendErr(error, "Failed to update plan"));
} finally {
setToggling(false);
}
@@ -148,21 +148,21 @@ export const ToggleDefaultProduct = ({
if (toggleKey === "is_default") {
if (value) {
setDialogDescription(
`You have ${activeCount} active ${activeCusStr} on this product. Are you sure you want to make this product default?`,
`You have ${activeCount} active ${activeCusStr} on this plan. Are you sure you want to make this plan default?`,
);
} else {
setDialogDescription(
`You have ${activeCount} active ${activeCusStr} on this product. Are you sure you want to remove this product as default?`,
`You have ${activeCount} active ${activeCusStr} on this plan. Are you sure you want to remove this plan as default?`,
);
}
} else {
if (value) {
setDialogDescription(
`You have ${activeCount} active ${activeCusStr} on this product. Are you sure you want to make this product an add-on?`,
`You have ${activeCount} active ${activeCusStr} on this plan. Are you sure you want to make this plan an add-on?`,
);
} else {
setDialogDescription(
`You have ${activeCount} active ${activeCusStr} on this product. Are you sure you want to remove this product as an add-on?`,
`You have ${activeCount} active ${activeCusStr} on this plan. Are you sure you want to remove this plan as an add-on?`,
);
}
}
@@ -180,11 +180,11 @@ export const ToggleDefaultProduct = ({
value && product.free_trial && !isFreeProductV2(product);
if (isDefaultTrial && notNullish(groupDefaults?.defaultTrial)) {
return `${groupDefaults.defaultTrial.name} is currently a default trial product. Making ${product.name} a default trial will remove ${groupDefaults.defaultTrial.name} as a default trial product.`;
return `${groupDefaults.defaultTrial.name} is currently a default trial plan. Making ${product.name} a default trial will remove ${groupDefaults.defaultTrial.name} as a default trial plan.`;
}
if (value && notNullish(groupDefaults?.free)) {
return `${groupDefaults.free.name} is currently a default product. Making ${product.name} a default product will remove ${groupDefaults.free.name} as a default product.`;
return `${groupDefaults.free.name} is currently a default plan. Making ${product.name} a default plan will remove ${groupDefaults.free.name} as a default plan.`;
}
};

View File

@@ -47,8 +47,8 @@ function CreatePlanDialog({
if (!/^[a-zA-Z0-9 _-]+$/.test(productName)) {
toast.error(
!productName
? "Product name is required"
: "Product name can only contain alphanumeric characters, dashes (-), and underscores (_)",
? "Plan name is required"
: "Plan name can only contain alphanumeric characters, dashes (-), and underscores (_)",
);
return;
}
@@ -67,7 +67,7 @@ function CreatePlanDialog({
}
setOpen(false);
} catch (error) {
toast.error(getBackendErr(error, "Failed to create product"));
toast.error(getBackendErr(error, "Failed to create plan"));
}
setLoading(false);
};
@@ -88,11 +88,11 @@ function CreatePlanDialog({
className={buttonClassName}
onClick={() => setOpen(true)}
>
Add Product
Add Plan
</IconButton>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="w-[500px]">
<DialogTitle>Create Product</DialogTitle>
<DialogTitle>Create Plan</DialogTitle>
<ProductConfig
product={product as unknown as ProductV2}
setProduct={setProduct}
@@ -107,7 +107,7 @@ function CreatePlanDialog({
onClick={handleCreateClicked}
variant="primary"
>
Create Product
Create Plan
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -46,8 +46,8 @@ function CreateProduct({
if (!/^[a-zA-Z0-9 _-]+$/.test(productName)) {
toast.error(
!productName
? "Product name is required"
: "Product name can only contain alphanumeric characters, dashes (-), and underscores (_)",
? "Plan name is required"
: "Plan name can only contain alphanumeric characters, dashes (-), and underscores (_)",
);
return;
}
@@ -66,7 +66,7 @@ function CreateProduct({
}
setOpen(false);
} catch (error) {
toast.error(getBackendErr(error, "Failed to create product"));
toast.error(getBackendErr(error, "Failed to create plan"));
}
setLoading(false);
};
@@ -83,11 +83,11 @@ function CreateProduct({
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="add" className="w-full">
Product
Plan
</Button>
</DialogTrigger>
<DialogContent className="w-[500px]">
<DialogTitle>Create Product</DialogTitle>
<DialogTitle>Create Plan</DialogTitle>
<ProductConfig
product={product}
setProduct={setProduct}
@@ -132,7 +132,7 @@ function CreateProduct({
variant="gradientPrimary"
className="min-w-44 w-44 max-w-44"
>
Create Product
Create Plan
</Button>
</div>
</DialogFooter>

View File

@@ -16,13 +16,13 @@ export const CreateProductMainDetails = () => {
});
return (
<SheetSection title="Product Details">
<SheetSection title="Plan Details">
<div className="space-y-4">
<div className="grid grid-cols-2 gap-2">
<div>
<FormLabel>Name</FormLabel>
<Input
placeholder="eg. Pro Product"
placeholder="eg. Pro Plan"
value={product.name}
onChange={(e) => setSource(e.target.value)}
/>