feat: 🎸 lots of stuff
This commit is contained in:
@@ -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>`
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -110,7 +110,7 @@ export const EditPlanHeader = () => {
|
||||
className="p-0"
|
||||
items={[
|
||||
{
|
||||
name: "Products",
|
||||
name: "Plans",
|
||||
href: "/products?tab=products",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function ProductViewBreadcrumbs() {
|
||||
onClick={() => navigateTo("/products", navigate, env)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Products
|
||||
Plans
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem className="cursor-pointer">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user