From a9db3f163f21a1bd811f34f78aee6d3898ec3391 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sun, 3 Aug 2025 08:05:30 -0700 Subject: [PATCH] adding code snippets for integration --- bun.lock | 6 +- server/src/external/stripe/stripeWebhooks.ts | 1 + .../src/internal/products/ProductService.ts | 4 +- .../handlers/handleListProductsBeta.ts | 8 - .../handleUpdateProduct.ts | 19 +- .../updateProductDetails.ts | 10 +- shared/index.ts | 3 + .../productV2Models/productResponseModels.ts | 1 + shared/utils/displayUtils.ts | 54 ++- shared/utils/featureUtils.ts | 32 ++ shared/utils/productDisplayUtils.ts | 202 ++++++++ .../utils/productDisplayUtils/getItemType.ts | 44 ++ .../productDisplayUtils/getProductItemRes.ts | 147 ++++++ .../productDisplayUtils/sortProductItems.ts | 47 ++ shared/utils/productUtils.ts | 85 ++++ shared/utils/utils.ts | 1 + vite/package.json | 4 +- vite/public/claude.png | Bin 0 -> 42457 bytes vite/public/cursor.png | Bin 0 -> 47702 bytes vite/src/app/layout.tsx | 16 +- .../src/components/autumn/checkout-dialog.tsx | 459 ++++++++++++++++++ vite/src/components/autumn/pricing-table.tsx | 81 ++-- vite/src/components/general/ToggleButton.tsx | 7 +- vite/src/components/ui/button.tsx | 23 +- vite/src/lib/autumn/checkout-content.tsx | 142 ++++++ vite/src/lib/autumn/pricing-table-content.tsx | 25 +- vite/src/utils/product/getItemType.ts | 5 +- vite/src/views/customers/CustomersView.tsx | 1 - .../src/views/onboarding2/OnboardingView2.tsx | 149 ++---- .../views/onboarding2/integrate/AITools.tsx | 74 +++ .../onboarding2/integrate/IntegrateAutumn.tsx | 66 +++ .../integrate/IntegrateContext.tsx | 15 + .../onboarding2/integrate/SelectStack.tsx | 183 +++++++ .../onboarding2/integrate/StepHeader.tsx | 16 + .../integration-steps/AutumnHandler.tsx | 137 ++++++ .../integrate/integration-steps/Install.tsx | 44 ++ .../integration-steps/handlerSnippets.ts | 74 +++ .../integration-steps/snippets/betterAuth.tsx | 33 ++ .../snippets/elysiaHandler.tsx | 26 + .../snippets/expressHandler.tsx | 48 ++ .../snippets/honoHandler.tsx | 106 ++++ .../snippets/nextjsHandler.tsx | 164 +++++++ .../integration-steps/snippets/rr7Handler.tsx | 73 +++ .../snippets/supabaseAuth.tsx | 35 ++ .../model-pricing/AddTrialButton.tsx | 64 +++ .../onboarding2/model-pricing/EditProduct.tsx | 215 ++++++++ .../model-pricing/EditProductOld.tsx | 192 ++++++++ .../model-pricing/ModelPricing.tsx | 259 ++++++++++ .../model-pricing/ModelPricingContext.tsx | 15 + .../model-pricing/SelectEditProduct.tsx | 64 +++ .../model-pricing-utils/modelPricingUtils.ts | 31 ++ .../productToPricingCard.tsx | 7 + .../model-pricing/usePricingTable.tsx | 17 + .../products/components/ProductRowToolbar.tsx | 82 ++-- .../product/free-trial/CreateFreeTrial.tsx | 16 +- .../product-item/CreateProductItem.tsx | 6 +- .../product-item/CreateProductItem2.tsx | 114 +++++ .../product-item/ProductItemConfig.tsx | 2 - .../product/product-item/ProductItemRow.tsx | 15 +- .../product/product-item/ProductItemTable.tsx | 147 +++--- .../product-item/UpdateProductItem.tsx | 6 +- .../components/SelectItemFeature.tsx | 28 +- .../CreateFeatureFromItem.tsx | 77 +++ .../CreateItemDialogContent.tsx | 151 ++++++ .../create-product-item/CreateItemIntro.tsx | 80 +++ .../create-product-item/defaultItemConfigs.ts | 54 +++ .../product-item-config/FeatureItemConfig.tsx | 4 +- .../item-config-footer/ItemConfigFooter.tsx | 116 +++-- 68 files changed, 4081 insertions(+), 351 deletions(-) create mode 100644 shared/utils/featureUtils.ts create mode 100644 shared/utils/productDisplayUtils.ts create mode 100644 shared/utils/productDisplayUtils/getItemType.ts create mode 100644 shared/utils/productDisplayUtils/getProductItemRes.ts create mode 100644 shared/utils/productDisplayUtils/sortProductItems.ts create mode 100644 shared/utils/productUtils.ts create mode 100644 vite/public/claude.png create mode 100644 vite/public/cursor.png create mode 100644 vite/src/components/autumn/checkout-dialog.tsx create mode 100644 vite/src/lib/autumn/checkout-content.tsx create mode 100644 vite/src/views/onboarding2/integrate/AITools.tsx create mode 100644 vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx create mode 100644 vite/src/views/onboarding2/integrate/IntegrateContext.tsx create mode 100644 vite/src/views/onboarding2/integrate/SelectStack.tsx create mode 100644 vite/src/views/onboarding2/integrate/StepHeader.tsx create mode 100644 vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx create mode 100644 vite/src/views/onboarding2/integrate/integration-steps/Install.tsx create mode 100644 vite/src/views/onboarding2/integrate/integration-steps/handlerSnippets.ts create mode 100644 vite/src/views/onboarding2/integrate/integration-steps/snippets/betterAuth.tsx create mode 100644 vite/src/views/onboarding2/integrate/integration-steps/snippets/elysiaHandler.tsx create mode 100644 vite/src/views/onboarding2/integrate/integration-steps/snippets/expressHandler.tsx create mode 100644 vite/src/views/onboarding2/integrate/integration-steps/snippets/honoHandler.tsx create mode 100644 vite/src/views/onboarding2/integrate/integration-steps/snippets/nextjsHandler.tsx create mode 100644 vite/src/views/onboarding2/integrate/integration-steps/snippets/rr7Handler.tsx create mode 100644 vite/src/views/onboarding2/integrate/integration-steps/snippets/supabaseAuth.tsx create mode 100644 vite/src/views/onboarding2/model-pricing/AddTrialButton.tsx create mode 100644 vite/src/views/onboarding2/model-pricing/EditProduct.tsx create mode 100644 vite/src/views/onboarding2/model-pricing/EditProductOld.tsx create mode 100644 vite/src/views/onboarding2/model-pricing/ModelPricing.tsx create mode 100644 vite/src/views/onboarding2/model-pricing/ModelPricingContext.tsx create mode 100644 vite/src/views/onboarding2/model-pricing/SelectEditProduct.tsx create mode 100644 vite/src/views/onboarding2/model-pricing/model-pricing-utils/modelPricingUtils.ts create mode 100644 vite/src/views/onboarding2/model-pricing/model-pricing-utils/productToPricingCard.tsx create mode 100644 vite/src/views/onboarding2/model-pricing/usePricingTable.tsx create mode 100644 vite/src/views/products/product/product-item/CreateProductItem2.tsx create mode 100644 vite/src/views/products/product/product-item/create-product-item/CreateFeatureFromItem.tsx create mode 100644 vite/src/views/products/product/product-item/create-product-item/CreateItemDialogContent.tsx create mode 100644 vite/src/views/products/product/product-item/create-product-item/CreateItemIntro.tsx create mode 100644 vite/src/views/products/product/product-item/create-product-item/defaultItemConfigs.ts diff --git a/bun.lock b/bun.lock index 8ec7a3e28..d4672561a 100644 --- a/bun.lock +++ b/bun.lock @@ -147,7 +147,7 @@ "@clerk/clerk-react": "^5.24.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", - "@radix-ui/react-accordion": "^1.2.3", + "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", @@ -167,7 +167,7 @@ "ag-charts-community": "^12.0.2", "ag-grid-community": "^34.0.2", "ag-grid-react": "^34.0.2", - "autumn-js": "^0.0.93", + "autumn-js": "^0.1.0", "axios": "^1.8.3", "better-auth": "^1.2.9", "class-variance-authority": "^0.7.1", @@ -2693,7 +2693,7 @@ "@autumn/vite/@types/node": ["@types/node@22.17.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ=="], - "@autumn/vite/autumn-js": ["autumn-js@0.0.93", "", { "dependencies": { "chalk": "^5.4.1", "rou3": "^0.6.1", "swr": "^2.3.3", "zod": "^3.24.1" }, "peerDependencies": { "better-auth": "^1.2.12", "better-call": "^1.0.12" }, "optionalPeers": ["better-auth", "better-call"] }, "sha512-zGq9cfkhHL6xAHlervcZHO2WQ28mHNoxKKI4vnkvKifW7ETDcQk0T7xdJS9g/a+BJh7CLqe697gEdMblMUDfUA=="], + "@autumn/vite/autumn-js": ["autumn-js@0.1.0", "", { "dependencies": { "rou3": "^0.6.1", "swr": "^2.3.3", "zod": "^3.24.1" }, "peerDependencies": { "better-auth": "^1.2.12", "better-call": "^1.0.12" }, "optionalPeers": ["better-auth", "better-call"] }, "sha512-7YLnc33xYUhWEP0yCuEwWliZ+EOqvIz6ZTc4GvVKYNQwC2UIXX5USsWKf9maKfxQF1zSEMRcCmCa3hBLRcNUZg=="], "@autumn/vite/date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], diff --git a/server/src/external/stripe/stripeWebhooks.ts b/server/src/external/stripe/stripeWebhooks.ts index 0810572e7..7a755dd70 100644 --- a/server/src/external/stripe/stripeWebhooks.ts +++ b/server/src/external/stripe/stripeWebhooks.ts @@ -204,6 +204,7 @@ stripeWebhookRouter.post( logger, }); break; + case "subscription_schedule.canceled": const canceledSchedule = event.data.object; await handleSubscriptionScheduleCanceled({ diff --git a/server/src/internal/products/ProductService.ts b/server/src/internal/products/ProductService.ts index 7c6a3418c..eb196d3af 100644 --- a/server/src/internal/products/ProductService.ts +++ b/server/src/internal/products/ProductService.ts @@ -265,12 +265,14 @@ export class ProductService { orgId, env, version, + allowNotFound = false, }: { db: DrizzleCli; idOrInternalId: string; orgId: string; env: AppEnv; version?: number; + allowNotFound?: boolean; }) { let data = (await db.query.products.findFirst({ where: and( @@ -298,7 +300,7 @@ export class ProductService { parseFreeTrials({ product: data }); if (!data) { - // return null; + if (allowNotFound) return null; throw new RecaseError({ message: `Product ${idOrInternalId} not found`, code: ErrCode.ProductNotFound, diff --git a/server/src/internal/products/handlers/handleListProductsBeta.ts b/server/src/internal/products/handlers/handleListProductsBeta.ts index 02759bca7..a8f57dd37 100644 --- a/server/src/internal/products/handlers/handleListProductsBeta.ts +++ b/server/src/internal/products/handlers/handleListProductsBeta.ts @@ -38,14 +38,6 @@ export const handleListProductsBeta = async (req: any, res: any) => env, logger: req.logger, }); - // return await CusService.getFull({ - // db, - // orgId: org.id, - // env, - // idOrInternalId: customerId as string, - // allowNotFound: true, - // entityId, - // }); })(), ]); diff --git a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts index bcf276daa..a19b6c53e 100644 --- a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts +++ b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts @@ -17,6 +17,7 @@ import { addTaskToQueue } from "@/queue/queueUtils.js"; import { JobName } from "@/queue/JobName.js"; import { productsAreSame } from "../../productUtils/compareProductUtils.js"; import { initProductInStripe } from "../../productUtils.js"; +import { handleCreateProduct } from "../handleCreateProduct.js"; export const handleUpdateProductV2 = async (req: any, res: any) => routeHandler({ @@ -25,7 +26,7 @@ export const handleUpdateProductV2 = async (req: any, res: any) => action: "Update product", handler: async () => { const { productId } = req.params; - const { version } = req.query; + const { version, upsert, disable_version } = req.query; const { orgId, env, logger, db } = req; const [features, org, fullProduct, rewardPrograms] = await Promise.all([ @@ -37,6 +38,7 @@ export const handleUpdateProductV2 = async (req: any, res: any) => orgId, env, version: version ? parseInt(version) : undefined, + allowNotFound: upsert == "true", }), RewardProgramService.getByProductId({ db, @@ -47,6 +49,12 @@ export const handleUpdateProductV2 = async (req: any, res: any) => ]); if (!fullProduct) { + console.log("Upserting:", upsert); + if (upsert == "true") { + await handleCreateProduct(req, res); + return; + } + throw new RecaseError({ message: "Product not found", code: ErrCode.ProductNotFound, @@ -75,6 +83,15 @@ export const handleUpdateProductV2 = async (req: any, res: any) => let itemsExist = notNullish(req.body.items); if (cusProductExists && itemsExist) { + if (disable_version == "true") { + throw new RecaseError({ + message: "Cannot auto save product as there are existing customers", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + return; + } + const { itemsSame, freeTrialsSame } = productsAreSame({ newProductV2: req.body, curProductV1: fullProduct, diff --git a/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts b/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts index bab6c4f14..26944a756 100644 --- a/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts +++ b/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts @@ -42,7 +42,13 @@ const productDetailsSame = (prod1: Product, prod2: UpdateProduct) => { if (notNullish(prod2.is_default) && prod1.is_default != prod2.is_default) { return false; } - console.log("prod1.archived", prod1.archived, prod2.archived, notNullish(prod2.archived), prod1.archived !== prod2.archived); + console.log( + "prod1.archived", + prod1.archived, + prod2.archived, + notNullish(prod2.archived), + prod1.archived !== prod2.archived + ); if (notNullish(prod2.archived) && prod1.archived !== prod2.archived) { return false; } @@ -71,7 +77,7 @@ const updateStripeProductNames = async ({ }); let stripeProdId = curProduct.processor?.id; - if (!stripeProdId) { + if (!stripeProdId || !newName) { return; } diff --git a/shared/index.ts b/shared/index.ts index 33fc04803..aa2cd2a81 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -140,6 +140,9 @@ export * from "./models/attachModels/attachBody.js"; export * from "./utils/displayUtils.js"; export * from "./models/checkModels/checkPreviewModels.js"; export * from "./models/chatResultModels/chatResultFeature.js"; +export * from "./utils/productDisplayUtils/getProductItemRes.js"; +export * from "./utils/productUtils.js"; +export * from "./utils/productDisplayUtils/sortProductItems.js"; // ENUMS export * from "./enums/SuccessCode.js"; diff --git a/shared/models/productV2Models/productResponseModels.ts b/shared/models/productV2Models/productResponseModels.ts index 375066017..401783454 100644 --- a/shared/models/productV2Models/productResponseModels.ts +++ b/shared/models/productV2Models/productResponseModels.ts @@ -31,3 +31,4 @@ export const ProductResponseSchema = z.object({ }); export type ProductResponse = z.infer; +export type ProductProperties = z.infer; diff --git a/shared/utils/displayUtils.ts b/shared/utils/displayUtils.ts index b0b4713d1..8919b7cf3 100644 --- a/shared/utils/displayUtils.ts +++ b/shared/utils/displayUtils.ts @@ -1,6 +1,7 @@ import { Feature } from "../models/featureModels/featureModels.js"; -import { Decimal } from "decimal.js"; +import { Organization } from "../models/orgModels/orgTable.js"; import { format } from "date-fns"; +import { notNullish, nullish } from "./utils.js"; export const getFeatureName = ({ feature, plural, @@ -19,18 +20,28 @@ export const getFeatureName = ({ let featureName = feature.name || ""; if (feature.display) { - if (units && units === 1) { - plural = false; + let finalPlural; + // Case 1: If units and nullish plural + if (notNullish(units) && nullish(plural)) { + finalPlural = units !== 1; + } else if (nullish(units) && notNullish(plural)) { + finalPlural = plural; + } else { + finalPlural = plural || false; } - // if (!plural) { - // if (units && units === 1) { - // plural = false; - // } else { - // plural = true; - // } - // } - if (plural) { + // if (units && units === 1) { + // plural = false; + // } + // // if (!plural) { + // // if (units && units === 1) { + // // plural = false; + // // } else { + // // plural = true; + // // } + // // } + + if (finalPlural) { featureName = feature.display.plural || featureName; } else { featureName = feature.display.singular || featureName; @@ -130,3 +141,24 @@ export const getFeatureInvoiceDescription = ({ return result; }; + +export const formatAmount = ({ + org, + currency, + amount, + maxFractionDigits = 2, + minFractionDigits = 0, +}: { + org?: Organization; + currency?: string | null; + amount: number; + maxFractionDigits?: number; + minFractionDigits?: number; +}) => { + return new Intl.NumberFormat(undefined, { + style: "currency", + currency: currency || org?.default_currency || "USD", + minimumFractionDigits: minFractionDigits || 0, + maximumFractionDigits: maxFractionDigits || 2, + }).format(amount); +}; diff --git a/shared/utils/featureUtils.ts b/shared/utils/featureUtils.ts new file mode 100644 index 000000000..0be2058fc --- /dev/null +++ b/shared/utils/featureUtils.ts @@ -0,0 +1,32 @@ +import { CreditSchemaItem } from "../models/featureModels/featureConfig/creditConfig.js"; +import { FeatureType } from "../models/featureModels/featureEnums.js"; +import { Feature } from "../models/featureModels/featureModels.js"; +import { APIFeatureSchema } from "../models/featureModels/featureResModels.js"; + +export const toAPIFeature = ({ feature }: { feature: Feature }) => { + // return FeatureResponseSchema.parse(feature); + // 1. Get feature type + let featureType = feature.type; + if (feature.type == FeatureType.Metered) { + featureType = feature.config.usage_type; + } + + let creditSchema = undefined; + if (feature.type == FeatureType.CreditSystem) { + creditSchema = feature.config.schema.map((s: CreditSchemaItem) => ({ + metered_feature_id: s.metered_feature_id, + credit_cost: s.credit_amount, + })); + } + + return APIFeatureSchema.parse({ + id: feature.id, + name: feature.name, + type: featureType, + display: { + singular: feature.display?.singular || feature.name, + plural: feature.display?.plural || feature.name, + }, + credit_schema: creditSchema, + }); +}; diff --git a/shared/utils/productDisplayUtils.ts b/shared/utils/productDisplayUtils.ts new file mode 100644 index 000000000..39b257f2f --- /dev/null +++ b/shared/utils/productDisplayUtils.ts @@ -0,0 +1,202 @@ +import { Feature } from "../models/featureModels/featureModels.js"; +import { Infinite } from "../models/productModels/productEnums.js"; +import { + ProductItem, + ProductItemFeatureType, +} from "../models/productV2Models/productItemModels/productItemModels.js"; +import { + getFeatureName, + numberWithCommas, + formatAmount, +} from "./displayUtils.js"; +import { + isFeatureItem, + isFeaturePriceItem, + isPriceItem, +} from "./productDisplayUtils/getItemType.js"; +import { notNullish, nullish } from "./utils.js"; + +export const formatTiers = ({ + item, + currency, +}: { + item: ProductItem; + currency?: string | null; +}) => { + let tiers = item.tiers; + if (tiers) { + if (tiers.length == 1) { + return formatAmount({ + currency, + amount: tiers[0].amount, + }); + } + + let firstPrice = tiers[0].amount; + let lastPrice = tiers[tiers.length - 1].amount; + + return `${formatAmount({ + currency, + amount: firstPrice, + })} - ${formatAmount({ + currency, + amount: lastPrice, + })}`; + } +}; + +export const getFeatureItemDisplay = ({ + item, + feature, +}: { + item: ProductItem; + feature?: Feature; +}) => { + if (!feature) { + throw new Error(`Feature ${item.feature_id} not found`); + } + // 1. If feature + if (item.feature_type == ProductItemFeatureType.Static) { + return { + primary_text: getFeatureName({ + feature, + plural: false, + capitalize: true, + }), + }; + } + + let featureName = getFeatureName({ + feature, + units: item.included_usage, + }); + + let includedUsageTxt = + item.included_usage == Infinite + ? "Unlimited " + : nullish(item.included_usage) || item.included_usage == 0 + ? "" + : `${numberWithCommas(item.included_usage!)} `; + + return { + primary_text: `${includedUsageTxt}${featureName}`, + }; +}; + +export const getPriceItemDisplay = ({ + item, + currency, +}: { + item: ProductItem; + currency?: string | null; +}) => { + let primaryText = formatAmount({ + currency, + amount: item.price as number, + }); + let secondaryText = item.interval ? `per ${item.interval}` : undefined; + + return { + primary_text: primaryText, + secondary_text: secondaryText, + }; +}; + +export const getFeaturePriceItemDisplay = ({ + feature, + item, + currency, + isMainPrice = false, + minifyIncluded = false, +}: { + feature?: Feature; + item: ProductItem; + currency?: string | null; + isMainPrice?: boolean; + minifyIncluded?: boolean; +}) => { + if (!feature) { + throw new Error(`Feature ${item.feature_id} not found`); + } + + // 1. Get included usage + let includedFeatureName = getFeatureName({ + feature, + units: item.included_usage, + }); + + let includedUsage = item.included_usage as number | null; + let includedUsageStr = ""; + if (notNullish(includedUsage) && includedUsage! > 0) { + if (minifyIncluded) { + includedUsageStr = `${numberWithCommas(includedUsage!)} included`; + } else { + includedUsageStr = `${numberWithCommas(includedUsage!)} ${includedFeatureName}`; + } + } + + let priceStr = formatTiers({ item, currency }); + let billingFeatureName = getFeatureName({ + feature, + units: item.billing_units, + }); + + let priceStr2 = ""; + if (item.billing_units && item.billing_units > 1) { + priceStr2 = `${numberWithCommas(item.billing_units)} ${billingFeatureName}`; + } else { + priceStr2 = `${billingFeatureName}`; + } + + let intervalStr = isMainPrice && item.interval ? ` per ${item.interval}` : ""; + + if (includedUsageStr) { + return { + primary_text: includedUsageStr, + secondary_text: `then ${priceStr} per ${priceStr2}${intervalStr}`, + }; + } + + return { + primary_text: priceStr + ` per ${priceStr2}${intervalStr}`, + // secondary_text: `per ${priceStr2}${intervalStr}`, + secondary_text: "", + }; +}; + +export const getProductItemDisplay = ({ + item, + features, + currency = "usd", +}: { + item: ProductItem; + features: Feature[]; + currency?: string | null; +}) => { + if (isFeatureItem(item)) { + return getFeatureItemDisplay({ + item, + feature: features.find((f) => f.id === item.feature_id), + }); + } + + if (isPriceItem(item)) { + return getPriceItemDisplay({ + item, + currency, + }); + } + + if (isFeaturePriceItem(item)) { + return getFeaturePriceItemDisplay({ + item, + feature: features.find((f) => f.id === item.feature_id), + currency, + }); + } + + return { + primary_text: "couldn't detect item type", + secondary_text: "", + }; +}; diff --git a/shared/utils/productDisplayUtils/getItemType.ts b/shared/utils/productDisplayUtils/getItemType.ts new file mode 100644 index 000000000..f106c631e --- /dev/null +++ b/shared/utils/productDisplayUtils/getItemType.ts @@ -0,0 +1,44 @@ +import { + ProductItem, + ProductItemType, +} from "../../models/productV2Models/productItemModels/productItemModels.js"; +import { notNullish, nullish } from "../utils.js"; + +export const isBooleanFeatureItem = (item: ProductItem) => { + return ( + notNullish(item.feature_id) && + (nullish(item.price) || item.price == 0) && + nullish(item.tiers) && + nullish(item.interval) && + nullish(item.included_usage) + ); +}; + +export const isFeatureItem = (item: ProductItem) => { + return ( + notNullish(item.feature_id) && + (nullish(item.price) || item.price == 0) && + nullish(item.tiers) + ); +}; + +export const isPriceItem = (item: ProductItem) => { + return notNullish(item.price) && nullish(item.feature_id); +}; + +export const isFeaturePriceItem = (item: ProductItem) => { + return ( + notNullish(item.feature_id) && + (notNullish(item.price) || notNullish(item.tiers)) + ); +}; + +export const getItemType = (item: ProductItem) => { + if (isFeatureItem(item)) { + return ProductItemType.Feature; + } else if (isFeaturePriceItem(item)) { + return ProductItemType.FeaturePrice; + } + + return ProductItemType.Price; +}; diff --git a/shared/utils/productDisplayUtils/getProductItemRes.ts b/shared/utils/productDisplayUtils/getProductItemRes.ts new file mode 100644 index 000000000..283c604ba --- /dev/null +++ b/shared/utils/productDisplayUtils/getProductItemRes.ts @@ -0,0 +1,147 @@ +import { FeatureOptions } from "../../models/cusProductModels/cusProductModels.js"; +import { Feature } from "../../models/featureModels/featureModels.js"; +import { ProductItemResponseSchema } from "../../models/productV2Models/productItemModels/prodItemResponseModels.js"; +import { + ProductItem, + UsageModel, +} from "../../models/productV2Models/productItemModels/productItemModels.js"; +import { toAPIFeature } from "../featureUtils.js"; +import { getProductItemDisplay } from "../productDisplayUtils.js"; +import { notNullish } from "../utils.js"; +import { getItemType } from "./getItemType.js"; +import { Decimal } from "decimal.js"; + +export const calculateProrationAmount = ({ + periodEnd, + periodStart, + now, + amount, + allowNegative = false, +}: { + periodEnd: number; + periodStart: number; + now: number; + amount: number; + allowNegative?: boolean; +}) => { + const num = new Decimal(periodEnd).minus(now); + const denom = new Decimal(periodEnd).minus(periodStart); + + const proratedAmount = num.div(denom).mul(amount); + + if (proratedAmount.lte(0) && !allowNegative) { + return 0; + } + + return proratedAmount.toNumber(); +}; + +export type Proration = { + start: number; + end: number; +}; + +export const itemToPriceOrTiers = ({ + item, + proration, + now, +}: { + item: ProductItem; + proration?: Proration; + now?: number; +}) => { + now = now || Date.now(); + if (item.price) { + return { + price: proration + ? calculateProrationAmount({ + periodEnd: proration.end, + periodStart: proration.start, + now, + amount: item.price, + }) + : item.price, + tiers: undefined, + }; + } else if (item.tiers) { + if (item.tiers.length > 1) { + return { + price: undefined, + tiers: item.tiers.map((tier) => ({ + ...tier, + amount: proration + ? calculateProrationAmount({ + periodEnd: proration.end, + periodStart: proration.start, + now, + amount: tier.amount, + }) + : tier.amount, + })), + }; + } else { + return { + price: proration + ? calculateProrationAmount({ + periodEnd: proration.end, + periodStart: proration.start, + now, + amount: item.tiers[0].amount, + }) + : item.tiers[0].amount, + tiers: undefined, + }; + } + } +}; + +export const getProductItemResponse = ({ + item, + features, + currency, + withDisplay = true, + options, +}: { + item: ProductItem; + features: Feature[]; + currency?: string | null; + withDisplay?: boolean; + options?: FeatureOptions[]; +}) => { + // 1. Get item type + let type = getItemType(item); + + // 2. Get display + let display = getProductItemDisplay({ + item, + features, + currency, + }); + + let priceData = itemToPriceOrTiers({ item }); + + let quantity = undefined; + let upcomingQuantity = undefined; + + if (item.usage_model == UsageModel.Prepaid && notNullish(options)) { + let option = options!.find((o) => o.feature_id == item.feature_id); + quantity = option?.quantity + ? option?.quantity * (item.billing_units ?? 1) + : undefined; + + upcomingQuantity = option?.upcoming_quantity + ? option?.upcoming_quantity * (item.billing_units ?? 1) + : undefined; + } + + let feature = features.find((f) => f.id == item.feature_id); + return ProductItemResponseSchema.parse({ + type, + ...item, + feature: feature ? toAPIFeature({ feature }) : null, + display: withDisplay ? display : undefined, + ...priceData, + quantity, + next_cycle_quantity: upcomingQuantity, + }); +}; diff --git a/shared/utils/productDisplayUtils/sortProductItems.ts b/shared/utils/productDisplayUtils/sortProductItems.ts new file mode 100644 index 000000000..36b0b7578 --- /dev/null +++ b/shared/utils/productDisplayUtils/sortProductItems.ts @@ -0,0 +1,47 @@ +import { Feature } from "../../models/featureModels/featureModels.js"; +import { ProductItem } from "../../models/productV2Models/productItemModels/productItemModels.js"; +import { isFeaturePriceItem, isPriceItem } from "./getItemType.js"; + +export const sortProductItems = (items: ProductItem[], features: Feature[]) => { + items.sort((a, b) => { + let aIsPriceItem = isPriceItem(a); + let bIsPriceItem = isPriceItem(b); + + if (aIsPriceItem && bIsPriceItem) { + return 0; + } + + if (aIsPriceItem && !bIsPriceItem) { + return -1; + } + + if (!aIsPriceItem && bIsPriceItem) { + return 1; + } + + // 2. Put feature price next + let aIsFeatureItem = isFeaturePriceItem(a); + let bIsFeatureItem = isFeaturePriceItem(b); + + if (aIsFeatureItem && !bIsFeatureItem) { + return -1; + } + + if (!aIsFeatureItem && bIsFeatureItem) { + return 1; + } + + // 3. Put feature price items in alphabetical order + let feature = features.find((f) => f.id == a.feature_id); + let aFeatureName = feature?.name; + let bFeatureName = features.find((f) => f.id == b.feature_id)?.name; + + if (!aFeatureName || !bFeatureName) { + return 0; + } + + return aFeatureName.localeCompare(bFeatureName); + }); + + return items; +}; diff --git a/shared/utils/productUtils.ts b/shared/utils/productUtils.ts new file mode 100644 index 000000000..1d9381f3d --- /dev/null +++ b/shared/utils/productUtils.ts @@ -0,0 +1,85 @@ +import { + BillingType, + Price, + ProductItem, + ProductResponse, + ProductV2, +} from "@autumn/shared"; +import { nullish } from "./utils.js"; +import { + isFeatureItem, + isFeaturePriceItem, + isPriceItem, +} from "./productDisplayUtils/getItemType.js"; +import { Decimal } from "decimal.js"; + +export const isFreeProductV2 = ({ items }: { items: ProductItem[] }) => { + return items.every((item) => nullish(item.price) && nullish(item.tiers)); +}; + +export const isProductUpgradeV2 = ({ + items1, + items2, +}: { + items1: ProductItem[]; + items2: ProductItem[]; +}) => { + if ( + !isFreeProductV2({ items: items1 }) && + isFreeProductV2({ items: items2 }) + ) { + return false; + } + + const prices1 = items1.filter( + (item) => isFeaturePriceItem(item) || isPriceItem(item) + ); + const prices2 = items2.filter( + (item) => isFeaturePriceItem(item) || isPriceItem(item) + ); + + // 2. Get total price for each product + const getTotalPrice = (items: ProductItem[]) => { + let totalPrice = new Decimal(0); + for (const item of items) { + if (item.price) totalPrice = totalPrice.plus(item.price); + if (item.tiers) { + let tierTotal = item.tiers.reduce( + (acc, tier) => acc.plus(tier.amount), + new Decimal(0) + ); + totalPrice = totalPrice.plus(tierTotal); + } + } + return totalPrice.toNumber(); + }; + + // Return interval...? + + return getTotalPrice(prices1) < getTotalPrice(prices2); +}; + +export const sortProductsV2 = ({ products }: { products: ProductV2[] }) => { + products.sort((a, b) => { + if (a.is_add_on !== b.is_add_on) { + return a.is_add_on ? 1 : -1; + } + + return 0; + }); + + return products.sort((a, b) => { + // Secondary sort: by add-on status (non-add-ons first) + if (a.is_add_on !== b.is_add_on) { + return a.is_add_on ? 1 : -1; + } + + // Primary sort: by price (using upgrade logic) + let isUpgrade = isProductUpgradeV2({ + items1: a.items, + items2: b.items, + }); + + return isUpgrade ? -1 : 1; + }); +}; diff --git a/shared/utils/utils.ts b/shared/utils/utils.ts index d954eec82..5f8b96b3a 100644 --- a/shared/utils/utils.ts +++ b/shared/utils/utils.ts @@ -1 +1,2 @@ export const notNullish = (value: any) => value !== null && value !== undefined; +export const nullish = (value: any) => value === null || value === undefined; diff --git a/vite/package.json b/vite/package.json index 3705d051e..f9efc0753 100644 --- a/vite/package.json +++ b/vite/package.json @@ -21,7 +21,7 @@ "@clerk/clerk-react": "^5.24.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", - "@radix-ui/react-accordion": "^1.2.3", + "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", @@ -41,7 +41,7 @@ "ag-charts-community": "^12.0.2", "ag-grid-community": "^34.0.2", "ag-grid-react": "^34.0.2", - "autumn-js": "^0.0.93", + "autumn-js": "^0.1.0", "axios": "^1.8.3", "better-auth": "^1.2.9", "class-variance-authority": "^0.7.1", diff --git a/vite/public/claude.png b/vite/public/claude.png new file mode 100644 index 0000000000000000000000000000000000000000..ed8571cabac43be5ce091264ca56f18641bde631 GIT binary patch literal 42457 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

c6Fg$%XCKH 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 literal 0 HcmV?d00001 diff --git a/vite/public/cursor.png b/vite/public/cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..85ae6f0ad6c45e96e4e07ee4367b28bb0b03904a GIT binary patch literal 47702 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⩔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 literal 0 HcmV?d00001 diff --git a/vite/src/app/layout.tsx b/vite/src/app/layout.tsx index 11e6e6cf4..38a72ff21 100644 --- a/vite/src/app/layout.tsx +++ b/vite/src/app/layout.tsx @@ -96,18 +96,20 @@ export function MainLayout() { // } return ( - +

- - - -
- - + + + + + ); diff --git a/vite/src/components/autumn/checkout-dialog.tsx b/vite/src/components/autumn/checkout-dialog.tsx new file mode 100644 index 000000000..ff0589667 --- /dev/null +++ b/vite/src/components/autumn/checkout-dialog.tsx @@ -0,0 +1,459 @@ +"use client"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import React, { useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogTitle, +} from "@/components/ui/dialog"; +import { getCheckoutContent } from "@/lib/autumn/checkout-content"; +import { useCustomer } from "autumn-js/react"; +import { ArrowRight, ChevronDown, Loader2 } from "lucide-react"; +import type { CheckoutResult, ProductItem } from "autumn-js"; +import { + Accordion, + AccordionContent, + AccordionItem, +} from "@/components/ui/accordion"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Input } from "@/components/ui/input"; + +export interface CheckoutDialogProps { + open: boolean; + setOpen: (open: boolean) => void; + checkoutResult: CheckoutResult; +} + +const formatCurrency = ({ + amount, + currency, +}: { + amount: number; + currency: string; +}) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: currency, + }).format(amount); +}; + +export default function CheckoutDialog(params: CheckoutDialogProps) { + const { attach } = useCustomer(); + const [checkoutResult, setCheckoutResult] = useState< + CheckoutResult | undefined + >(params?.checkoutResult); + + useEffect(() => { + if (params.checkoutResult) { + setCheckoutResult(params.checkoutResult); + } + }, [params.checkoutResult]); + + const [loading, setLoading] = useState(false); + + if (!checkoutResult) { + return <>; + } + + const { open, setOpen } = params; + const { title, message } = getCheckoutContent(checkoutResult); + + const isFree = checkoutResult?.product.properties?.is_free; + const isPaid = isFree === false; + + return ( + + + {title} +
+ {message} +
+ + {isPaid && checkoutResult && ( + + )} + + + + +
+
+ ); +} + +function PriceInformation({ + checkoutResult, + setCheckoutResult, +}: { + checkoutResult: CheckoutResult; + setCheckoutResult: (checkoutResult: CheckoutResult) => void; +}) { + return ( +
+ + +
+ {checkoutResult?.has_prorations && checkoutResult.lines.length > 0 && ( + + )} + +
+
+ ); +} + +function DueAmounts({ checkoutResult }: { checkoutResult: CheckoutResult }) { + const { next_cycle, product } = checkoutResult; + const nextCycleAtStr = next_cycle + ? new Date(next_cycle.starts_at).toLocaleDateString() + : undefined; + + const hasUsagePrice = product.items.some( + (item) => item.usage_model === "pay_per_use" + ); + + const showNextCycle = next_cycle && next_cycle.total !== checkoutResult.total; + + return ( +
+
+
+

Total due today

+
+ +

+ {formatCurrency({ + amount: checkoutResult?.total, + currency: checkoutResult?.currency, + })} +

+
+ {showNextCycle && ( +
+
+

Due next cycle ({nextCycleAtStr})

+
+

+ {formatCurrency({ + amount: next_cycle.total, + currency: checkoutResult?.currency, + })} + {hasUsagePrice && + usage prices} +

+
+ )} +
+ ); +} + +function ProductItems({ + checkoutResult, + setCheckoutResult, +}: { + checkoutResult: CheckoutResult; + setCheckoutResult: (checkoutResult: CheckoutResult) => void; +}) { + const isUpdateQuantity = + checkoutResult?.product.scenario === "active" && + checkoutResult.product.properties.updateable; + return ( +
+

Price

+ {checkoutResult?.product.items + .filter((item) => item.type !== "feature") + .map((item, index) => { + if (item.usage_model == "prepaid") { + return ( + + ); + } + + if (isUpdateQuantity) { + return null; + } + + return ( +
+

+ {item.feature ? item.feature.name : "Subscription"} +

+

+ {item.display?.primary_text} {item.display?.secondary_text} +

+
+ ); + })} +
+ ); +} + +function CheckoutLines({ checkoutResult }: { checkoutResult: CheckoutResult }) { + return ( + + + +
+

+ View details +

+ +
+
+ + {checkoutResult?.lines + .filter((line) => line.amount != 0) + .map((line, index) => { + return ( +
+

{line.description}

+

+ {new Intl.NumberFormat("en-US", { + style: "currency", + currency: checkoutResult?.currency, + }).format(line.amount)} +

+
+ ); + })} +
+
+
+ ); +} + +function CustomAccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + ); +} + +const PrepaidItem = ({ + item, + checkoutResult, + setCheckoutResult, +}: { + item: ProductItem; + checkoutResult: CheckoutResult; + setCheckoutResult: (checkoutResult: CheckoutResult) => void; +}) => { + const { quantity = 0, billing_units: billingUnits = 1 } = item; + const [quantityInput, setQuantityInput] = useState( + (quantity / billingUnits).toString() + ); + const { checkout } = useCustomer(); + const [loading, setLoading] = useState(false); + const [open, setOpen] = useState(false); + const scenario = checkoutResult.product.scenario; + + const handleSave = async () => { + setLoading(true); + try { + const newOptions = checkoutResult.options + .filter((option) => option.feature_id !== item.feature_id) + .map((option) => { + return { + featureId: option.feature_id, + quantity: option.quantity, + }; + }); + + newOptions.push({ + featureId: item.feature_id!, + quantity: Number(quantityInput) * billingUnits, + }); + + const { data, error } = await checkout({ + productId: checkoutResult.product.id, + options: newOptions, + }); + + if (error) { + console.error(error); + return; + } + setCheckoutResult(data!); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + setOpen(false); + } + }; + + const disableSelection = scenario === "renew"; + + return ( +
+
+

{item.feature?.name}

+ + + Qty: {quantity} + + + +
+

{item.feature?.name}

+

+ {item.display?.primary_text} {item.display?.secondary_text} +

+
+ +
+
+ setQuantityInput(e.target.value)} + /> +

+ {billingUnits > 1 && `x ${billingUnits} `} + {item.feature?.name} +

+
+ + +
+
+
+
+

+ {item.display?.primary_text} {item.display?.secondary_text} +

+
+ ); +}; + +export const PriceItem = ({ + children, + className, + ...props +}: { + children: React.ReactNode; + className?: string; +} & React.HTMLAttributes) => { + return ( +
+ {children} +
+ ); +}; + +export const PricingDialogButton = ({ + children, + size, + onClick, + disabled, + className, +}: { + children: React.ReactNode; + size?: "sm" | "lg" | "default" | "icon"; + onClick: () => void; + disabled?: boolean; + className?: string; +}) => { + return ( + + ); +}; diff --git a/vite/src/components/autumn/pricing-table.tsx b/vite/src/components/autumn/pricing-table.tsx index 7517353fd..7d1aa2298 100644 --- a/vite/src/components/autumn/pricing-table.tsx +++ b/vite/src/components/autumn/pricing-table.tsx @@ -1,34 +1,37 @@ import React from "react"; -import { useCustomer, usePricingTable } from "autumn-js/react"; + +import { useCustomer, usePricingTable, ProductDetails } from "autumn-js/react"; import { createContext, useContext, useState } from "react"; import { cn } from "@/lib/utils"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; -import { Check, Loader2 } from "lucide-react"; -import AttachDialog from "@/components/autumn/attach-dialog"; +import CheckoutDialog from "@/components/autumn/checkout-dialog"; import { getPricingTableContent } from "@/lib/autumn/pricing-table-content"; -import { Product, ProductItem } from "autumn-js"; +import type { Product, ProductItem } from "autumn-js"; +import { Loader2 } from "lucide-react"; export default function PricingTable({ productDetails, + products, }: { - productDetails?: any; + productDetails?: ProductDetails[]; + products?: Product[]; }) { - const { attach } = useCustomer(); + const { checkout } = useCustomer(); const [isAnnual, setIsAnnual] = useState(false); - const { products, isLoading, error } = usePricingTable({ productDetails }); + const { isLoading, error } = usePricingTable({ productDetails }); - if (isLoading) { - return ( -
- -
- ); - } + // if (isLoading) { + // return ( + //
+ // + //
+ // ); + // } - if (error) { - return
Something went wrong...
; - } + // if (error) { + // return
Something went wrong...
; + // } const intervals = Array.from( new Set( @@ -38,7 +41,7 @@ export default function PricingTable({ const multiInterval = intervals.length > 1; - const intervalFilter = (product: any) => { + const intervalFilter = (product: Product) => { if (!product.properties?.interval_group) { return true; } @@ -55,10 +58,10 @@ export default function PricingTable({ }; return ( -
+
{products && ( { if (product.id) { - await attach({ + await checkout({ productId: product.id, - dialog: AttachDialog, - openInNewTab: true, - successUrl: window.location.href, + dialog: CheckoutDialog, }); } else if (product.display?.button_url) { window.open(product.display?.button_url, "_blank"); @@ -162,7 +165,8 @@ export const PricingTableContainer = ({ )}
@@ -194,9 +198,10 @@ export const PricingCard = ({ throw new Error(`Product with id ${productId} not found`); } - const { name, display: productDisplay, items } = product; + const { name, display: productDisplay } = product; const { buttonText } = getPricingTableContent(product); + const isRecommended = productDisplay?.recommend_text ? true : false; const mainPriceDisplay = product.properties?.is_free ? { @@ -211,7 +216,7 @@ export const PricingCard = ({ return (
-

- {productDisplay?.name || name} +

+ {productDisplay?.name || name || ( + + Name this product + + )}

{productDisplay?.description && (
@@ -239,7 +248,7 @@ export const PricingCard = ({ )}
-

+

{mainPriceDisplay?.primary_text}{" "} {mainPriceDisplay?.secondary_text && ( @@ -255,7 +264,6 @@ export const PricingCard = ({
@@ -266,7 +274,7 @@ export const PricingCard = ({ recommended={productDisplay?.recommend_text ? true : false} {...buttonProps} > - {buttonText} + {productDisplay?.button_text || buttonText}

@@ -277,12 +285,10 @@ export const PricingCard = ({ // Pricing Feature List export const PricingFeatureList = ({ items, - showIcon = true, everythingFrom, className, }: { items: ProductItem[]; - showIcon?: boolean; everythingFrom?: string; className?: string; }) => { @@ -293,10 +299,7 @@ export const PricingFeatureList = ({ )}
{items.map((item, index) => ( -
- {showIcon && ( - - )} +
{item.display?.primary_text} {item.display?.secondary_text && ( diff --git a/vite/src/components/general/ToggleButton.tsx b/vite/src/components/general/ToggleButton.tsx index ce6cf2eb1..369f1698c 100644 --- a/vite/src/components/general/ToggleButton.tsx +++ b/vite/src/components/general/ToggleButton.tsx @@ -26,7 +26,11 @@ export const ToggleButton = ({ */} - { - await mutate(); - // setProduct(newProduct); - // setOpen(true); - }} - /> - - } - className="pr-0 border-l" - /> - { - const selectedProduct = data.products.find( - (p: ProductV2) => p.id === id - ); - setProduct(selectedProduct); - setOriginalProduct( - JSON.parse(JSON.stringify(selectedProduct)) - ); - setOpen(true); - }} - /> - -
-

Integrate Autumn

@@ -175,15 +122,9 @@ export default function OnboardingView2() {

- - {/* */}
-
+
*/} ); } diff --git a/vite/src/views/onboarding2/integrate/AITools.tsx b/vite/src/views/onboarding2/integrate/AITools.tsx new file mode 100644 index 000000000..6322f8cab --- /dev/null +++ b/vite/src/views/onboarding2/integrate/AITools.tsx @@ -0,0 +1,74 @@ +import { StepHeader } from "./StepHeader"; +import { Button } from "@/components/ui/button"; +import CopyButton from "@/components/general/CopyButton"; +import { ExternalLink, Download } from "lucide-react"; +import { toast } from "sonner"; + +export const AITools = () => { + // MCP configuration for Autumn + const mcpConfig = { + name: "autumn", + command: "npx", + args: ["-y", "mcp-remote", "https://docs.useautumn.com/mcp"], + }; + + // Base64 encode the configuration for Cursor's install URL + const encodedConfig = btoa(JSON.stringify(mcpConfig)); + const cursorInstallUrl = `https://cursor.com/install-mcp?name=autumn&config=${encodedConfig}`; + + // Manual JSON configuration for copy-paste + const manualConfig = JSON.stringify( + { + mcpServers: { + autumn: mcpConfig, + }, + }, + null, + 2 + ); + + const handleCursorInstall = () => { + window.open(cursorInstallUrl, "_blank"); + }; + + return ( +
+ +

+ If you're using Cursor or Claude Code, you can install our MCP server to + use AI to integrate Autumn. +

+ + {/* One-click install for Cursor */} +
+ + +
+
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx b/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx new file mode 100644 index 000000000..3dfe12419 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx @@ -0,0 +1,66 @@ +import { AITools } from "./AITools"; +import { SelectStack } from "./SelectStack"; +import { useState } from "react"; +import { IntegrateContext } from "./IntegrateContext"; +import { notNullish } from "@/utils/genUtils"; +import { Install } from "./integration-steps/Install"; +import { AutumnHandler } from "./integration-steps/AutumnHandler"; +import { parseAsString, parseAsJson, useQueryStates } from "nuqs"; + +export default function IntegrateAutumn() { + const [stack, setStack] = useState<{ + frontend: string; + backend: string; + auth: string; + customerType: string; + }>({ + frontend: "", + backend: "", + auth: "", + customerType: "", + }); + + const [queryStates, setQueryStates] = useQueryStates({ + frontend: parseAsString.withDefault(""), + backend: parseAsString.withDefault(""), + auth: parseAsString.withDefault(""), + customerType: parseAsString.withDefault(""), + }); + + const stackSelected = Object.values(stack).every(notNullish); + + return ( + +
+
+
+

Integrate Autumn

+

+ Let's integrate Autumn and get your first customer onto one of + your plans +

+
+ +
+ + + {stackSelected && ( + <> + + + + )} +
+ {/*
+ +

+ Create a .env file in the root of your project and add the following + environment variables: +

+ +
*/} +
+
+
+ ); +} diff --git a/vite/src/views/onboarding2/integrate/IntegrateContext.tsx b/vite/src/views/onboarding2/integrate/IntegrateContext.tsx new file mode 100644 index 000000000..8f16ae543 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/IntegrateContext.tsx @@ -0,0 +1,15 @@ +import { createContext, useContext } from "react"; + +export const IntegrateContext = createContext(null); + +export const useIntegrateContext = () => { + const context = useContext(IntegrateContext); + + if (context === undefined) { + throw new Error( + "useProductContext must be used within a ProductContextProvider" + ); + } + + return context; +}; diff --git a/vite/src/views/onboarding2/integrate/SelectStack.tsx b/vite/src/views/onboarding2/integrate/SelectStack.tsx new file mode 100644 index 000000000..a28cff3e2 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/SelectStack.tsx @@ -0,0 +1,183 @@ +import { useState } from "react"; +import { StepHeader } from "./StepHeader"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useIntegrateContext } from "./IntegrateContext"; + +export const SelectStack = () => { + const { queryStates, setQueryStates } = useIntegrateContext(); + + const frontendOptions = [ + { value: "nextjs", label: "Next.js" }, + { value: "vite", label: "Vite SPA" }, + { value: "tanstack", label: "Tanstack Start" }, + { value: "non-react", label: "Non-React" }, + ]; + + const backendOptions = [ + { value: "nextjs", label: "Next.js" }, + { value: "react_router", label: "React Router 7" }, + { value: "hono", label: "Hono" }, + { value: "express", label: "Express" }, + { value: "elysia", label: "Elysia" }, + { value: "supabase", label: "Supabase" }, + { value: "convex", label: "Convex" }, + ]; + + const authOptions = [ + { value: "better_auth", label: "Better Auth" }, + { value: "supabase", label: "Supabase Auth" }, + { value: "clerk", label: "Clerk" }, + { value: "other", label: "Other" }, + ]; + + const customerOptions = [ + { value: "user", label: "Users" }, + { value: "org", label: "Organizations" }, + // { value: "other", label: "Other (eg. Projects / Workspaces)" }, + ]; + + return ( +
+ +

+ Help us customize the integration guide for your specific tech stack. +

+ +
+ {/* Frontend Framework */} +
+ + +
+ + {/* Backend Framework */} +
+ + +
+ + {/* Auth Provider */} +
+ + +
+ + {/* Customer Type */} +
+ + +
+
+ + {/* {(stack.frontend || + stack.backend || + stack.auth || + stack.customerType) && ( +
+

Selected Stack:

+
+ {stack.frontend && ( +

+ Frontend:{" "} + {frontendOptions.find((f) => f.value === stack.frontend)?.label} +

+ )} + {stack.backend && ( +

+ Backend:{" "} + {backendOptions.find((b) => b.value === stack.backend)?.label} +

+ )} + {stack.auth && ( +

+ Auth:{" "} + {authOptions.find((a) => a.value === stack.auth)?.label} +

+ )} + {stack.customerType && ( +

+ Customers:{" "} + { + customerOptions.find((c) => c.value === stack.customerType) + ?.label + } +

+ )} +
+
+ )} */} +
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/StepHeader.tsx b/vite/src/views/onboarding2/integrate/StepHeader.tsx new file mode 100644 index 000000000..8ac04206d --- /dev/null +++ b/vite/src/views/onboarding2/integrate/StepHeader.tsx @@ -0,0 +1,16 @@ +export const StepHeader = ({ + number, + title, +}: { + number: number; + title: string; +}) => { + return ( +
+
+ {number} +
+

{title}

+
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx new file mode 100644 index 000000000..6346ba3fb --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx @@ -0,0 +1,137 @@ +import CodeBlock from "@/views/onboarding/components/CodeBlock"; +import { StepHeader } from "../StepHeader"; +import { useIntegrateContext } from "../IntegrateContext"; + +import {} from "./handlerSnippets"; +import { + nextjsBetterAuthOrg, + nextjsBetterAuthUser, + nextjsClerkOrg, + nextjsClerkUser, + nextjsOther, + nextjsSupabaseOrg, + nextjsSupabaseUser, +} from "./snippets/nextjsHandler"; +import { rr7BetterAuth, rr7Clerk, rr7Supabase } from "./snippets/rr7Handler"; +import { + honoBetterAuth, + honoClerk, + honoSupabase, +} from "./snippets/honoHandler"; +import { + expressBetterAuth, + expressClerk, + expressSupabase, +} from "./snippets/expressHandler"; +import { elysiaBetterAuth } from "./snippets/elysiaHandler"; + +const snippet = () => { + return { + nextjs: { + ["better_auth"]: { + user: nextjsBetterAuthUser, + org: nextjsBetterAuthOrg, + }, + ["supabase"]: { + user: nextjsSupabaseUser, + org: nextjsSupabaseOrg, + }, + ["clerk"]: { + user: nextjsClerkUser, + org: nextjsClerkOrg, + }, + ["other"]: { + user: nextjsOther, + org: nextjsOther, + }, + }, + ["react_router"]: { + ["better_auth"]: { + user: rr7BetterAuth("user"), + org: rr7BetterAuth("org"), + }, + ["supabase"]: { + user: rr7Supabase("user"), + org: rr7Supabase("org"), + }, + ["clerk"]: { + user: rr7Clerk("user"), + org: rr7Clerk("org"), + }, + }, + ["hono"]: { + ["clerk"]: { + user: honoClerk("user"), + org: honoClerk("org"), + }, + ["supabase"]: { + user: honoSupabase("user"), + org: honoSupabase("org"), + }, + ["better_auth"]: { + user: honoBetterAuth("user"), + org: honoBetterAuth("org"), + }, + }, + ["express"]: { + ["better_auth"]: { + user: expressBetterAuth("user"), + org: expressBetterAuth("org"), + }, + ["clerk"]: { + user: expressClerk("user"), + org: expressClerk("org"), + }, + ["supabase"]: { + user: expressSupabase("user"), + org: expressSupabase("org"), + }, + }, + ["elysia"]: { + ["better_auth"]: { + user: elysiaBetterAuth("user"), + org: elysiaBetterAuth("org"), + }, + }, + } as any; +}; + +export const AutumnHandler = () => { + const { queryStates } = useIntegrateContext(); + + const getSnippetContent = () => { + const backendLang = queryStates.backend; + const authProvider = queryStates.auth; + const customerType = queryStates.customerType; + + const templates = snippet(); + + console.log("Backend Lang", backendLang); + console.log("Auth Provider", authProvider); + + const template = + templates[backendLang]?.[authProvider]?.[customerType] || ""; + + console.log("Template", template); + + // Trim \n from the template (only left and right) + return template.trim("\n"); + }; + + return ( +
+ + + +
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx b/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx new file mode 100644 index 000000000..d364d7ce1 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx @@ -0,0 +1,44 @@ +import CodeBlock from "@/views/onboarding/components/CodeBlock"; +import { StepHeader } from "../StepHeader"; + +const installCode = `npm install autumn-js`; +const installCodePnpm = `pnpm add autumn-js`; +const installCodeYarn = `yarn add autumn-js`; +const installCodeBun = `bun add autumn-js`; + +export const Install = () => { + return ( +
+ + + +
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/handlerSnippets.ts b/vite/src/views/onboarding2/integrate/integration-steps/handlerSnippets.ts new file mode 100644 index 000000000..d5477ad2b --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/handlerSnippets.ts @@ -0,0 +1,74 @@ +export const nextjsBetterAuthUser = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + return { + customerId: session?.user.id, + customerData: { + name: session?.user.name, + email: session?.user.email, + }, + }; + }, +}); +`; + +export const nextjsBetterAuthOrg = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + return { + customerId: session?.session.activeOrganizationId, + customerData: { + name: session?.user.name, + email: session?.user.email, + }, + }; + }, +}); +`; + +export const nextjsBetterAuthOther = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + // From the request, retrieve the ID you want to use as the customer ID + const customerId = "customer_id"; + + return { + customerId, + customerData: { + name: session?.user.name, + email: session?.user.email, + }, + }; + }, +}); +`; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/betterAuth.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/betterAuth.tsx new file mode 100644 index 000000000..6c356fbba --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/betterAuth.tsx @@ -0,0 +1,33 @@ +export const betterAuthSnippet = ( + customerType: "user" | "org", + headersString: string, + tabLevel: number = 0 +) => { + const tabs = " ".repeat(tabLevel); + + let snippet = ``; + if (customerType === "user") { + snippet = `${tabs}const session = await auth.api.getSession({ +${tabs} headers: ${headersString}, +${tabs}}); +${tabs} +${tabs}return { +${tabs} customerId: session?.user.id, +${tabs} customerData: { +${tabs} name: session?.user.name, +${tabs} email: session?.user.email, +${tabs} }, +${tabs}};`; + } else { + snippet = `${tabs}const session = await auth.api.getSession({ +${tabs} headers: ${headersString}, +${tabs}}); +${tabs} +${tabs}return { +${tabs} customerId: session?.session.activeOrganizationId, +${tabs} customerData: { name: "", email: "" } +${tabs}};`; + } + + return snippet; +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/elysiaHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/elysiaHandler.tsx new file mode 100644 index 000000000..8b1862693 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/elysiaHandler.tsx @@ -0,0 +1,26 @@ +import { betterAuthSnippet } from "./betterAuth"; + +export const elysiaBetterAuth = (customerType: "user" | "org") => { + return `import { autumnHandler } from "autumn-js/elysia"; +import { auth } from "./auth"; + +const app = new Elysia({ adapter: node() }) + .use(cors()) + .mount(auth.handler) + .use( + autumnHandler({ + identify: async (context) => { +${betterAuthSnippet(customerType, "context.headers", 4)} + }, + }) + ) + .listen(8000); + + `; +}; + +// export const elysiaOther = (customerType: "user" | "org") => { +// return ` + +// `; +// }; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/expressHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/expressHandler.tsx new file mode 100644 index 000000000..b251636f2 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/expressHandler.tsx @@ -0,0 +1,48 @@ +import { betterAuthSnippet } from "./betterAuth"; +import { clerkSnippet, supabaseSnippet } from "./honoHandler"; + +export const expressBetterAuth = (customerType: "user" | "org") => { + return `import { autumnHandler } from "autumn-js/express"; +import { auth } from "@/lib/auth"; + +app.use(express.json()); // need to parse request body before autumnHandler +app.use( + "/api/autumn", + autumnHandler({ + identify: async (req) => { +${betterAuthSnippet(customerType, "req.headers", 3)} + }, + }) +);`; +}; + +export const expressClerk = (customerType: "user" | "org") => { + return `import { autumnHandler } from "autumn-js/express"; +import { clerkMiddleware, getAuth } from "@clerk/express"; + +app.use(express.json()); // need to parse request body before autumnHandler +app.use(clerkMiddleware()); +app.use( + "/api/autumn", + autumnHandler({ + identify: async (req) => { + ${clerkSnippet(customerType, "req")} + }, + }) +);`; +}; + +export const expressSupabase = (customerType: "user" | "org") => { + return `import { autumnHandler } from "autumn-js/express"; +import { createClient } from "./lib/supabase"; + +app.use(express.json()); // need to parse request body before autumnHandler +app.use( + "/api/autumn", + autumnHandler({ + identify: async (req, res) => { + ${supabaseSnippet(customerType, "createClient({ req, res })")} + }, + }) +);`; +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/honoHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/honoHandler.tsx new file mode 100644 index 000000000..b323421a3 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/honoHandler.tsx @@ -0,0 +1,106 @@ +import { betterAuthSnippet } from "./betterAuth"; + +export const clerkSnippet = ( + customerType: "user" | "org", + reqParam: string = "c" +) => { + if (customerType === "user") { + return `const auth = getAuth(${reqParam}); + + if (!auth?.userId) return null; + + return { + customerId: auth.userId, + customerData: { name: "", email: "" }, + }; + `; + } + + return `const auth = getAuth(${reqParam}); + + if (!auth?.userId || !auth?.orgId) return null; + + return { + customerId: auth.orgId, + customerData: { name: "", email: "" }, + }; + `; +}; + +export const honoClerk = (customerType: "user" | "org") => { + return `import { autumnHandler } from "autumn-js/hono"; +import { clerkMiddleware, getAuth } from "@hono/clerk-auth"; + +app.use("*", clerkMiddleware()); +app.use( + "/api/autumn/*", + autumnHandler({ + identify: async (c: Context) => { + ${clerkSnippet(customerType)} + }, + }) +);`; +}; + +export const supabaseSnippet = ( + customerType: "user" | "org", + supabaseInit: string = "getSupabase(c)" +) => { + if (customerType === "user") { + return `const supabase = ${supabaseInit}; + + const { data, error } = await supabase.auth.getUser(); + + if (!data?.user?.id) return null; + + return { + customerId: data.user.id, + customerData: { name: "", email: "" }, + };`; + } + + return `const supabase = ${supabaseInit}; + + const { data, error } = await supabase.auth.getUser(); + + if (!data?.user?.id) return null; + + const orgId = "users_org_id"; // Get the orgId from your DB + + return { + customerId: orgId, + customerData: { name: "", email: "" }, + };`; +}; + +export const honoSupabase = (customerType: "user" | "org") => { + return `// index.ts + +import { autumnHandler } from "autumn-js/hono"; +import { getSupabase, supabaseMiddleware } from "./middleware/auth.middleware.js"; + +app.use("*", supabaseMiddleware()); +app.use( + "/api/autumn/*", + autumnHandler({ + identify: async (c: Context) => { + ${supabaseSnippet(customerType)} + }, + }) +);`; +}; +export const honoBetterAuth = (customerType: "user" | "org") => { + return `// index.ts + +import { autumnHandler } from "autumn-js/hono"; +import { auth } from "@/lib/auth" + +app.use( + "/api/autumn/*", + autumnHandler({ + identify: async (c: Context) => { +${betterAuthSnippet(customerType, "c.req.raw.headers", 3)} + }, + }) +);`; +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/nextjsHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/nextjsHandler.tsx new file mode 100644 index 000000000..035cf0348 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/nextjsHandler.tsx @@ -0,0 +1,164 @@ +export const nextjsBetterAuthUser = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + return { + customerId: session?.user.id, + customerData: { + name: session?.user.name, + email: session?.user.email, + }, + }; + }, +}); +`; + +export const nextjsBetterAuthOrg = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + return { + customerId: session?.session.activeOrganizationId, + customerData: { + name: session?.user.name, + email: session?.user.email, + }, + }; + }, +}); +`; + +// Supabase Auth +export const nextjsSupabaseUser = ` +// app/api/autumn/[...all]/route.ts + +import { createClient } from "@/utils/supabase/server"; +import { autumnHandler } from "autumn-js/next"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const supabase = await createClient(); + const { data, error } = await supabase.auth.getUser(); + + if (error || !data?.user) { + return null; + } + + return { + customerId: data.user.id, + customerData: { + name: data.user.user_metadata?.name, + email: data.user.email, + }, + }; + }, +});`; + +export const nextjsSupabaseOrg = ` +// app/api/autumn/[...all]/route.ts + +import { createClient } from "@/utils/supabase/server"; +import { autumnHandler } from "autumn-js/next"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const supabase = await createClient(); + const { data, error } = await supabase.auth.getUser(); + + if (error || !data?.user) { + return null; + } + + // Get the orgId of the user from your DB + const customerId = "users_org_id"; + + return { + customerId, + customerData: { + name: data.user.user_metadata?.name, + email: data.user.email, + }, + }; + }, +}); +`; + +export const nextjsClerkUser = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@clerk/nextjs/server"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const { userId } = await auth(); + + if (!userId) return null; + + return { + customerId: userId, + // To store the customer name and email + customerData: { name: "", email: "" }, + }; + }, +}); +`; + +export const nextjsClerkOrg = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@clerk/nextjs/server"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const { userId, orgId } = await auth(); + + if (!userId || !orgId) return null; + + return { + customerId: orgId, + // To store the customer name and email + customerData: { name: "", email: "" }, + }; + }, +}); +`; + +export const nextjsOther = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; + +export const { GET, POST } = autumnHandler({ + identify: async (request) => { + + // Authenticate the request and get the customer ID + const customerId = "customer_id"; + + return { + customerId, + // To store the customer name and email + customerData: { name: "", email: "" }, + }; + }, +}); +`; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/rr7Handler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/rr7Handler.tsx new file mode 100644 index 000000000..9cc41764c --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/rr7Handler.tsx @@ -0,0 +1,73 @@ +import { betterAuthSnippet } from "./betterAuth"; +import { supabaseAuthSnippet } from "./supabaseAuth"; + +export const rr7BetterAuth = (customerType: "user" | "org") => { + return `// app/routes/api.autumn.tsx + +import { autumnHandler } from "autumn-js/react-router"; +import { auth } from "@/lib/auth"; + +const handler = autumnHandler({ + secretKey: process.env.AUTUMN_SECRET_KEY!, + identify: async (args) => { +${betterAuthSnippet(customerType, "args.request.headers", 2)} + }, +}); + +export const loader = handler.loader; +export const action = handler.action;`; +}; +export const rr7Supabase = (customerType: "user" | "org") => { + return `// app/routes/api.autumn.tsx + +import { autumnHandler } from "autumn-js/react-router"; +import { createClient } from "@/utils/supabase/server"; + +const handler = autumnHandler({ + secretKey: process.env.AUTUMN_SECRET_KEY!, + identify: async (args) => { + ${supabaseAuthSnippet({ customerType })} + }, +}); + +export const loader = handler.loader; +export const action = handler.action;`; +}; + +export const clerkSnippet = (customerType: "user" | "org") => { + if (customerType === "user") { + return `const { userId } = await getAuth(args); + + if (!userId) return null; + + return { + customerId: userId, + customerData: { name: "", email: "" }, + };`; + } + + return `const { userId, orgId } = await getAuth(args); + + if (!userId || !orgId) return null; + + return { + customerId: orgId, + customerData: { name: "", email: "" }, + };`; +}; +export const rr7Clerk = (customerType: "user" | "org") => { + return `// app/routes/api.autumn.tsx + +import { autumnHandler } from "autumn-js/react-router"; +import { getAuth } from "@clerk/react-router/ssr.server"; + +const handler = autumnHandler({ + secretKey: process.env.AUTUMN_SECRET_KEY!, + identify: async (args) => { + ${clerkSnippet(customerType)} + }, +}); + +export const loader = handler.loader; +export const action = handler.action;`; +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/supabaseAuth.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/supabaseAuth.tsx new file mode 100644 index 000000000..9c7087120 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/supabaseAuth.tsx @@ -0,0 +1,35 @@ +export const supabaseAuthSnippet = ({ + backendLang, + customerType, +}: { + backendLang?: string; + customerType: "user" | "org"; +}) => { + if (customerType === "user") { + return `const supabase = await createClient(request); + const { data, error } = await supabase.auth.getUser(); + + if (error || !data?.user) return null; + + return { + customerId: "123", + customerData: { + name: data.user.user_metadata?.name, + email: data.user.email, + }, + };`; + } + + return `const supabase = await createClient(request); + const { data, error } = await supabase.auth.getUser(); + + if (error || !data?.user) return null; + + // Get the orgId from your DB + const orgId = "users_org_id"; + + return { + customerId: orgId, + customerData: { name: "", email: "" }, + };`; +}; diff --git a/vite/src/views/onboarding2/model-pricing/AddTrialButton.tsx b/vite/src/views/onboarding2/model-pricing/AddTrialButton.tsx new file mode 100644 index 000000000..551dad797 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/AddTrialButton.tsx @@ -0,0 +1,64 @@ +import { Button } from "@/components/ui/button"; +import { CreateFreeTrial } from "@/views/products/product/free-trial/CreateFreeTrial"; +import { useState } from "react"; +import { useProductContext } from "@/views/products/product/ProductContext"; +import { PlusIcon, Trash, X } from "lucide-react"; +import { handleAutoSave } from "./model-pricing-utils/modelPricingUtils"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; + +export const AddTrialButton = () => { + const { product, setProduct, mutate } = useProductContext(); + const [open, setOpen] = useState(false); + const axiosInstance = useAxiosInstance(); + + return ( + <> + + +
+ ) : ( +

+ Add Free Trial + +

+ )} + + + ); +}; diff --git a/vite/src/views/onboarding2/model-pricing/EditProduct.tsx b/vite/src/views/onboarding2/model-pricing/EditProduct.tsx new file mode 100644 index 000000000..d01b73d41 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/EditProduct.tsx @@ -0,0 +1,215 @@ +import FieldLabel from "@/components/general/modal-components/FieldLabel"; +import { ToggleButton } from "@/components/general/ToggleButton"; +import { Input } from "@/components/ui/input"; +import { slugify } from "@/utils/formatUtils/formatTextUtils"; +import { FeaturesContext } from "@/views/features/FeaturesContext"; +import { CreateFreeTrial } from "@/views/products/product/free-trial/CreateFreeTrial"; +import { CreateProductItem2 } from "@/views/products/product/product-item/CreateProductItem2"; +import { ProductItemTable } from "@/views/products/product/product-item/ProductItemTable"; +import { ProductContext } from "@/views/products/product/ProductContext"; +import { Button } from "@/components/ui/button"; +import { AddTrialButton } from "./AddTrialButton"; +import { useEffect, useState } from "react"; +import { useEnv } from "@/utils/envUtils"; +import { handleAutoSave } from "./model-pricing-utils/modelPricingUtils"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { useModelPricingContext } from "./ModelPricingContext"; +import { ProductRowToolbar } from "@/views/products/components/ProductRowToolbar"; + +export const EditProduct = ({ + data, + mutate, + product, + setProduct, +}: { + data: any; + mutate: any; + product: any; + setProduct: any; +}) => { + const [details, setDetails] = useState({ + name: product.name, + id: product.id, + }); + + const [freeTrialModalOpen, setFreeTrialModalOpen] = useState(false); + const [entityFeatureIds, setEntityFeatureIds] = useState([]); + const [features, setFeatures] = useState([]); + const { productCounts, editingNewProduct, setEditingNewProduct } = + useModelPricingContext(); + const axiosInstance = useAxiosInstance(); + const env = useEnv(); + + useEffect(() => { + if (data) { + setFeatures(data.features); + } + }, [data]); + + const hasItems = product.items.length > 0; + + const handleToggleSettings = async (key: string) => { + const curValue = product[key]; + const newProduct = { ...product, [key]: !curValue }; + + setProduct(newProduct); + + handleAutoSave({ + axiosInstance, + productId: product.id ? product.id : details.id, + product: { ...product, [key]: !curValue }, + mutate, + }); + }; + + return ( +
+
+ + +
+
+
+
+ + Name + + { + await handleAutoSave({ + axiosInstance, + productId: product.id ? product.id : details.id, + product: { + ...product, + name: details.name, + id: details.id, + }, + mutate, + }); + setProduct({ + ...product, + name: details.name, + id: details.id, + }); + }} + placeholder="Free Plan" + value={details.name} + onChange={(e) => { + const curProduct = data?.products.find( + (p: any) => p.id === details.id + ); + console.log("Cur product:", curProduct); + const newIdData = editingNewProduct + ? { + id: slugify(e.target.value), + } + : {}; + setDetails({ + ...details, + name: e.target.value, + ...newIdData, + }); + }} + /> +
+
+ ID + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+
+
+ handleToggleSettings("is_default")} + /> +
+ A default product is enabled by default for all new users, + typically used for your free plan. +
+
+
+ handleToggleSettings("is_add_on")} + /> +
+ A default product is enabled by default for all new users, + typically used for your free plan. +
+
+
+ +
+ Add a free trial to your product. +
+
+
+
+
+
+
+
+ ); +}; diff --git a/vite/src/views/onboarding2/model-pricing/EditProductOld.tsx b/vite/src/views/onboarding2/model-pricing/EditProductOld.tsx new file mode 100644 index 000000000..2f088a441 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/EditProductOld.tsx @@ -0,0 +1,192 @@ +import { DialogFooter } from "@/components/ui/dialog"; +import { ProductService } from "@/services/products/ProductService"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { useEnv } from "@/utils/envUtils"; +import { getBackendErr } from "@/utils/genUtils"; +import { FeaturesContext } from "@/views/features/FeaturesContext"; +import { CreateFreeTrial } from "@/views/products/product/free-trial/CreateFreeTrial"; +import { ManageProduct } from "@/views/products/product/ManageProduct"; +import { ProductContext } from "@/views/products/product/ProductContext"; +import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; +import { TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; +import { Check, X } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Tooltip } from "@/components/ui/tooltip"; +import { toast } from "sonner"; +import { ToggleButton } from "@/components/general/ToggleButton"; + +export const EditProductDialog = ({ + product, + features, + setProduct, + setFeatures, + mutate, + open, + setOpen, + originalProduct, + entityFeatureIds, + setEntityFeatureIds, +}: { + product: any; + setProduct: (product: any) => void; + features: any[]; + setFeatures: (features: any[]) => void; + mutate: () => Promise; + open: boolean; + setOpen: (open: boolean) => void; + originalProduct: any; + entityFeatureIds: string[]; + setEntityFeatureIds: (entityFeatureIds: string[]) => void; +}) => { + const env = useEnv(); + const axiosInstance = useAxiosInstance(); + const [createProductLoading, setCreateProductLoading] = useState(false); + const [freeTrialModalOpen, setFreeTrialModalOpen] = useState(false); + + // Store the original product state when modal opens + const handleOpenChange = async (newOpen: boolean) => { + if (!newOpen && open && product?.id) { + // Modal is being closed, check if there are changes + const hasChanges = + originalProduct && + JSON.stringify(product) !== JSON.stringify(originalProduct); + + if (hasChanges) { + // Only update if there are changes + updateProduct(); + } + } + setOpen(newOpen); + }; + + const updateProduct = async () => { + setCreateProductLoading(true); + try { + const res = await ProductService.updateProduct( + axiosInstance, + product.id, + product + ); + toast.success("Product updated successfully"); + await mutate(); + setOpen(false); + } catch (error) { + toast.error(getBackendErr(error, "Failed to update product")); + } + setCreateProductLoading(false); + }; + + const handleFreeTrialClick = () => { + if (product?.free_trial) { + // Delete the free trial + setProduct({ ...product, free_trial: null }); + } else { + // Open the free trial modal + setFreeTrialModalOpen(true); + } + }; + + return ( + + + + {/* Edit Product */} + +
+ + + + + + +
+ +
+
+ + setProduct({ + ...product, + is_default: !product?.is_default, + }) + } + /> + + setProduct({ ...product, is_add_on: !product?.is_add_on }) + } + /> + +
+ ) : ( + //
+ + // {/* */} + //
+

Add Free Trial

+ )} + +
+ +
+ + + + ); +}; diff --git a/vite/src/views/onboarding2/model-pricing/ModelPricing.tsx b/vite/src/views/onboarding2/model-pricing/ModelPricing.tsx new file mode 100644 index 000000000..ae529b184 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/ModelPricing.tsx @@ -0,0 +1,259 @@ +import PricingTable from "@/components/autumn/pricing-table"; +import { EditProduct } from "./EditProduct"; +import { useEffect, useState } from "react"; +import { + getProductItemResponse, + ProductProperties, + ProductV2, + sortProductsV2, + UsageModel, +} from "@autumn/shared"; +import { getBackendErr, notNullish, nullish } from "@/utils/genUtils"; +import { isFreeProduct, isOneOffProduct } from "@/utils/product/priceUtils"; +import { Product } from "autumn-js"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { PlusIcon } from "lucide-react"; +import { Tabs, TabsTrigger, TabsList } from "@/components/ui/tabs"; +import { + ModelPricingContext, + useModelPricingContext, +} from "./ModelPricingContext"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Input } from "@/components/ui/input"; +import { slugify } from "@/utils/formatUtils/formatTextUtils"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { toast } from "sonner"; +import { ProductsContext } from "@/views/products/ProductsContext"; +import { SelectEditProduct } from "./SelectEditProduct"; + +const defaultProduct = { + id: "", + name: "", + items: [], + is_default: false, + is_add_on: false, + free_trial: null, +}; + +export const ModelPricing = ({ + data, + mutate, + autumnProducts, + productCounts, +}: { + data: any; + mutate: any; + autumnProducts: Product[]; + productCounts: any; +}) => { + const curProduct = autumnProducts.length > 0 ? autumnProducts[0] : null; + + const [product, setProduct] = useState( + (curProduct && + data.products.find((p: ProductV2) => p.id === autumnProducts[0].id)) || + (defaultProduct as unknown as ProductV2) + ); + + const [firstItemCreated, setFirstItemCreated] = useState( + autumnProducts.some((p: Product) => p.items.length > 0) + ); + + const [editingNewProduct, setEditingNewProduct] = useState( + nullish(curProduct) + ); + + // Get latest product + const getAutumnProducts = () => { + const curProductItems = product.items.map((item: any) => + getProductItemResponse({ + item, + features: data.features, + currency: "USD", + }) + ); + + return autumnProducts; + // return [product, ...autumnProducts]; + + // const properties: ProductProperties = { + // has_trial: notNullish(product.free_trial), + // is_free: isFreeProduct(product.items), + // is_one_off: isOneOffProduct(product.items), + // updateable: product.items.some( + // (item: any) => item.usage_model == UsageModel.Prepaid + // ), + // }; + + // const latestProduct = { + // ...product, + // items: curProductItems, + // properties, + // }; + + // const curProducts = autumnProducts.filter( + // (p: Product) => p.id !== product.id + // ); + + // if (!firstItemCreated) { + // return []; + // } + + // const newProducts = [latestProduct, ...curProducts] as any; + // return sortProductsV2({ products: newProducts }) as Product[]; + }; + + useEffect(() => { + if (data) { + const curProduct = data.products.find( + (p: Product) => p.id === product.id + ); + + if (!curProduct) { + if (data.products.length > 0) { + setProduct(data.products[0]); + } + } + } + }, [data]); + + return ( + + +
+
+
+
+

Create your plans

+ {firstItemCreated && ( +
+ + +
+ )} +
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+ ); +}; + +const NewProductPopover = () => { + const [open, setOpen] = useState(false); + const { mutate, data, setProduct } = useModelPricingContext(); + + const axiosInstance = useAxiosInstance(); + const [details, setDetails] = useState({ + name: "", + id: "", + }); + + const [creating, setCreating] = useState(false); + + const handleSave = async () => { + try { + setCreating(true); + await axiosInstance.post("/v1/products", { + name: details.name, + id: details.id, + }); + await mutate(); + const newProduct = { + ...defaultProduct, + name: details.name, + id: details.id, + }; + setProduct(newProduct); + setOpen(false); + } catch (error) { + toast.error(getBackendErr(error, "Failed to create product")); + } finally { + setCreating(false); + } + }; + + return ( + + + + + +
+

New Product

+
+ + setDetails({ + ...details, + name: e.target.value, + id: slugify(e.target.value), + }) + } + /> + +
+
+ +
+
+
+
+ ); +}; diff --git a/vite/src/views/onboarding2/model-pricing/ModelPricingContext.tsx b/vite/src/views/onboarding2/model-pricing/ModelPricingContext.tsx new file mode 100644 index 000000000..a1ac58699 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/ModelPricingContext.tsx @@ -0,0 +1,15 @@ +import { createContext, useContext } from "react"; + +export const ModelPricingContext = createContext(null); + +export const useModelPricingContext = () => { + const context = useContext(ModelPricingContext); + + if (context === undefined) { + throw new Error( + "useProductContext must be used within a ProductContextProvider" + ); + } + + return context; +}; diff --git a/vite/src/views/onboarding2/model-pricing/SelectEditProduct.tsx b/vite/src/views/onboarding2/model-pricing/SelectEditProduct.tsx new file mode 100644 index 000000000..176453160 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/SelectEditProduct.tsx @@ -0,0 +1,64 @@ +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useModelPricingContext } from "./ModelPricingContext"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { ChevronDownIcon } from "lucide-react"; + +export const SelectEditProduct = () => { + const { data, product, setProduct } = useModelPricingContext(); + + if (data.products.length > 3) { + return ( + + + + + + {data.products.map((p: any) => { + if (!p.name) { + return null; + } + return ( + setProduct(p)}> + {p.name} + + ); + })} + + + ); + } + + const tabTriggerClass = + "data-[state=active]:bg-stone-200 data-[state=active]:text-t2 data-[state=active]:font-medium"; + + return ( + + + {data.products.map((p: any) => { + if (!p.name) { + return null; + } + return ( + setProduct(p)} + > + {p.name} + + ); + })} + + + ); +}; diff --git a/vite/src/views/onboarding2/model-pricing/model-pricing-utils/modelPricingUtils.ts b/vite/src/views/onboarding2/model-pricing/model-pricing-utils/modelPricingUtils.ts new file mode 100644 index 000000000..bbf3dd218 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/model-pricing-utils/modelPricingUtils.ts @@ -0,0 +1,31 @@ +import { getBackendErr, notNullish } from "@/utils/genUtils"; +import { ProductV2 } from "@autumn/shared"; +import { AxiosInstance } from "axios"; +import { toast } from "sonner"; + +export const handleAutoSave = async ({ + axiosInstance, + productId, + product, + mutate, +}: { + axiosInstance: AxiosInstance; + productId: string; + product: ProductV2; + mutate: any; +}) => { + if (!productId || !product.id) return; + try { + await axiosInstance.post( + `v1/products/${productId}?upsert=true&disable_version=true`, + { + ...product, + group: notNullish(product.group) ? product.group : undefined, + } + ); + await mutate(); + } catch (error) { + console.log(error); + toast.error(getBackendErr(error, "Failed to auto save product")); + } +}; diff --git a/vite/src/views/onboarding2/model-pricing/model-pricing-utils/productToPricingCard.tsx b/vite/src/views/onboarding2/model-pricing/model-pricing-utils/productToPricingCard.tsx new file mode 100644 index 000000000..587eae21f --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/model-pricing-utils/productToPricingCard.tsx @@ -0,0 +1,7 @@ +export const productToPricingCard = (product: any) => { + return { + id: product.id, + name: product.name, + items: product.items, + }; +}; diff --git a/vite/src/views/onboarding2/model-pricing/usePricingTable.tsx b/vite/src/views/onboarding2/model-pricing/usePricingTable.tsx new file mode 100644 index 000000000..60352f603 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/usePricingTable.tsx @@ -0,0 +1,17 @@ +import { useAxiosSWR } from "@/services/useAxiosSwr"; + +export const useListProducts = () => { + const { data, isLoading, error, mutate } = useAxiosSWR({ + url: "/v1/products", + options: { + refreshInterval: 0, + }, + }); + + return { + products: data?.list || [], + isLoading, + error, + mutate, + }; +}; diff --git a/vite/src/views/products/components/ProductRowToolbar.tsx b/vite/src/views/products/components/ProductRowToolbar.tsx index dc305d212..53475bd62 100644 --- a/vite/src/views/products/components/ProductRowToolbar.tsx +++ b/vite/src/views/products/components/ProductRowToolbar.tsx @@ -22,10 +22,12 @@ import { DeleteProductDialog } from "./DeleteProductDialog"; export const ProductRowToolbar = ({ product, productCounts, + isOnboarding = false, }: { className?: string; product: Product; productCounts: ProductCounts; + isOnboarding?: boolean; }) => { const [deleteLoading, setDeleteLoading] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); @@ -69,44 +71,48 @@ export const ProductRowToolbar = ({ - - { - e.stopPropagation(); - e.preventDefault(); - setSelectedProduct(product); - setDialogType("copy"); - setModalOpen(true); - }} - > -
- Copy - {copyLoading ? ( - - ) : ( - - )} -
-
-
- - { - e.stopPropagation(); - e.preventDefault(); - setSelectedProduct(product); - setDialogType("update"); - setModalOpen(true); - }} - > -
- Edit - -
-
-
+ {!isOnboarding && ( + + { + e.stopPropagation(); + e.preventDefault(); + setSelectedProduct(product); + setDialogType("copy"); + setModalOpen(true); + }} + > +
+ Copy + {copyLoading ? ( + + ) : ( + + )} +
+
+
+ )} + {!isOnboarding && ( + + { + e.stopPropagation(); + e.preventDefault(); + setSelectedProduct(product); + setDialogType("update"); + setModalOpen(true); + }} + > +
+ Edit + +
+
+
+ )} { diff --git a/vite/src/views/products/product/free-trial/CreateFreeTrial.tsx b/vite/src/views/products/product/free-trial/CreateFreeTrial.tsx index 52d9ad153..377e378a8 100644 --- a/vite/src/views/products/product/free-trial/CreateFreeTrial.tsx +++ b/vite/src/views/products/product/free-trial/CreateFreeTrial.tsx @@ -10,6 +10,8 @@ import { useProductContext } from "../ProductContext"; import { FreeTrialConfig } from "./FreeTrialConfig"; import { toast } from "sonner"; import { FreeTrialDuration } from "@autumn/shared"; +import { handleAutoSave } from "@/views/onboarding2/model-pricing/model-pricing-utils/modelPricingUtils"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; export const CreateFreeTrial = ({ open, @@ -21,7 +23,10 @@ export const CreateFreeTrial = ({ // const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [price, setPrice] = useState(null); - const { env, product, setProduct, prices } = useProductContext(); + const { env, product, setProduct, prices, autoSave, mutate } = + useProductContext(); + + const axiosInstance = useAxiosInstance(); const [freeTrial, setFreeTrial] = useState({ length: 7, @@ -44,6 +49,15 @@ export const CreateFreeTrial = ({ duration: freeTrial.duration, }, }); + + if (autoSave) { + handleAutoSave({ + axiosInstance, + productId: product.id, + product: { ...product, free_trial: freeTrial }, + mutate, + }); + } setOpen(false); }; diff --git a/vite/src/views/products/product/product-item/CreateProductItem.tsx b/vite/src/views/products/product/product-item/CreateProductItem.tsx index fdbd6618b..3965bc517 100644 --- a/vite/src/views/products/product/product-item/CreateProductItem.tsx +++ b/vite/src/views/products/product/product-item/CreateProductItem.tsx @@ -11,19 +11,17 @@ import { useState } from "react"; import { ProductItemConfig } from "./ProductItemConfig"; import { ProductItemContext } from "./ProductItemContext"; import { CreateFeature } from "@/views/features/CreateFeature"; + import { ProductItemInterval, ProductItem, CreateFeature as CreateFeatureType, - UpdateProductSchema, } from "@autumn/shared"; import { useProductContext } from "../ProductContext"; import { validateProductItem } from "@/utils/product/product-item/validateProductItem"; - import { DialogContentWrapper } from "@/components/general/modal-components/DialogContentWrapper"; import { ItemConfigFooter } from "./product-item-config/item-config-footer/ItemConfigFooter"; -import { ProductService } from "@/services/products/ProductService"; import { useAxiosInstance } from "@/services/useAxiosInstance"; export const defaultProductItem: ProductItem = { @@ -173,7 +171,7 @@ export function CreateProductItem() { (features.length == 0 && item.price === null) ? (
) : ( - + {}} /> )} diff --git a/vite/src/views/products/product/product-item/CreateProductItem2.tsx b/vite/src/views/products/product/product-item/CreateProductItem2.tsx new file mode 100644 index 000000000..be9850657 --- /dev/null +++ b/vite/src/views/products/product/product-item/CreateProductItem2.tsx @@ -0,0 +1,114 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; + +import { useEffect, useState } from "react"; +import { ProductItemConfig } from "./ProductItemConfig"; +import { ProductItemContext } from "./ProductItemContext"; +import { CreateFeature } from "@/views/features/CreateFeature"; + +import { + ProductItemInterval, + ProductItem, + CreateFeature as CreateFeatureType, +} from "@autumn/shared"; + +import { useProductContext } from "../ProductContext"; +import { validateProductItem } from "@/utils/product/product-item/validateProductItem"; +import { DialogContentWrapper } from "@/components/general/modal-components/DialogContentWrapper"; +import { ItemConfigFooter } from "./product-item-config/item-config-footer/ItemConfigFooter"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { PlusIcon } from "lucide-react"; +import { CreateItemDialogContent } from "./create-product-item/CreateItemDialogContent"; +import { useModelPricingContext } from "@/views/onboarding2/model-pricing/ModelPricingContext"; + +const defaultProductItem: ProductItem = { + feature_id: null, + + included_usage: null, + + interval: ProductItemInterval.Month, + + // Price config + price: null, + tiers: null, + billing_units: 1, + + // Others + entity_feature_id: null, + reset_usage_when_enabled: true, +}; + +export function CreateProductItem2() { + const [open, setOpen] = useState(false); + const [showCreateFeature, setShowCreateFeature] = useState(false); + const [item, setItem] = useState(defaultProductItem); + const { features, product, setProduct, setFeatures } = useProductContext(); + const { firstItemCreated, setFirstItemCreated } = useModelPricingContext(); + + const handleCreateProductItem = async (entityFeatureId?: string) => { + const validatedItem = validateProductItem({ + item: { + ...item, + entity_feature_id: entityFeatureId + ? entityFeatureId + : item.entity_feature_id, + }, + features, + }); + + if (!validatedItem) return; + + const newItems = [...product.items, validatedItem]; + const newProduct = { ...product, items: newItems }; + setProduct(newProduct); + setTimeout(() => { + setItem({ + ...defaultProductItem, + feature_id: null, + }); + }, 400); + + setOpen(false); + setFirstItemCreated(true); + return newProduct; + }; + + return ( + + +
+ + + +
+ +
+
+ ); +} diff --git a/vite/src/views/products/product/product-item/ProductItemConfig.tsx b/vite/src/views/products/product/product-item/ProductItemConfig.tsx index 27b4d8ceb..daa7c74a5 100644 --- a/vite/src/views/products/product/product-item/ProductItemConfig.tsx +++ b/vite/src/views/products/product/product-item/ProductItemConfig.tsx @@ -22,9 +22,7 @@ import { isFeaturePriceItem, isPriceItem } from "@/utils/product/getItemType"; export const ProductItemConfig = () => { // HOOKS const { features } = useProductContext(); - const { item, setItem } = useProductItemContext(); - const [show, setShow] = useState(getShowParams(item)); const handleAddPrice = () => { diff --git a/vite/src/views/products/product/product-item/ProductItemRow.tsx b/vite/src/views/products/product/product-item/ProductItemRow.tsx index 75e5139ea..619b1ba38 100644 --- a/vite/src/views/products/product/product-item/ProductItemRow.tsx +++ b/vite/src/views/products/product/product-item/ProductItemRow.tsx @@ -21,6 +21,7 @@ import { Flag } from "lucide-react"; import { cn } from "@/lib/utils"; import { isFeatureItem, isPriceItem } from "@/utils/product/getItemType"; import { notNullish } from "@/utils/genUtils"; +import { useProductContext } from "../ProductContext"; interface ProductItemRowProps { item: ProductItem; @@ -29,6 +30,7 @@ interface ProductItemRowProps { features: Feature[]; org: any; onRowClick: (item: ProductItem, index: number) => void; + className?: string; } export const ProductItemRow = ({ @@ -38,7 +40,9 @@ export const ProductItemRow = ({ features, org, onRowClick, + className, }: ProductItemRowProps) => { + const { product } = useProductContext(); const getName = ({ featureId, units, @@ -200,13 +204,15 @@ export const ProductItemRow = ({ }; const itemType = getItemType(item); + const isLast = index === product.items.length - 1; return (
onRowClick(item, index)} > @@ -226,7 +232,12 @@ export const ProductItemRow = ({ : getPaidFeatureString(item)} - + { - const { product, setProduct, features, org, entityFeatureIds } = +export const ProductItemTable = () => { + const { product, features, org, entityFeatureIds, isOnboarding } = useProductContext(); const [selectedItem, setSelectedItem] = useState(null); const [selectedIndex, setSelectedIndex] = useState(null); const [open, setOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); - const [entitiesOpen, setEntitiesOpen] = useState(false); const [freeTrialOpen, setFreeTrialOpen] = useState(false); const handleRowClick = (item: ProductItem, index: number) => { @@ -44,11 +39,11 @@ export const ProductItemTable = ({ const groupedItems = entityFeatureIds.reduce( (acc: Record, entityFeatureId: string) => { acc[entityFeatureId] = product.items.filter( - (item: ProductItem) => item.entity_feature_id === entityFeatureId, + (item: ProductItem) => item.entity_feature_id === entityFeatureId ); return acc; }, - {} as Record, + {} as Record ); return ( @@ -64,65 +59,81 @@ export const ProductItemTable = ({

Product Items

+
-
- - - -
+ {/*
*/} + {!isOnboarding && } + + +
*/}
+ + {/* */}
-
+ +
{/* Original product items mapping - excluding items that appear in grouped sections */} {product.items .filter( (item: ProductItem) => !entityFeatureIds.some( (entityFeatureId: string) => - item.entity_feature_id === entityFeatureId, - ), + item.entity_feature_id === entityFeatureId + ) ) .map((item: ProductItem, index: number) => ( -

+

{entityFeatureId}

@@ -163,7 +179,7 @@ export const ProductItemTable = ({ org={org} onRowClick={handleRowClick} /> - ), + ) )} {/* Show message if no items for this entityFeatureId */} @@ -171,7 +187,7 @@ export const ProductItemTable = ({
Add the features this entity gets access to @@ -181,8 +197,17 @@ export const ProductItemTable = ({ ))} {product.items.length === 0 && ( -
+

+ Product items determine what customers get access to and how + they're billed. Start by adding one. +

+ {/*

Product items determine what customers get access to and how they're billed{" "} Priced Features:{" "} features that have a price based on usage (eg, $1 per credit)

-
+
*/}
)}
diff --git a/vite/src/views/products/product/product-item/UpdateProductItem.tsx b/vite/src/views/products/product/product-item/UpdateProductItem.tsx index 5a13461b0..584d7f86c 100644 --- a/vite/src/views/products/product/product-item/UpdateProductItem.tsx +++ b/vite/src/views/products/product/product-item/UpdateProductItem.tsx @@ -28,18 +28,19 @@ export default function UpdateProductItem({ const [showCreateFeature, setShowCreateFeature] = useState(false); const handleUpdateProductItem = () => { - console.log("Selected Item: ", selectedItem); const validatedItem = validateProductItem({ item: selectedItem!, features, }); - if (!validatedItem) return; + if (!validatedItem) return null; if (notNullish(selectedIndex)) { const newProduct = { ...product }; newProduct.items[selectedIndex!] = validatedItem; setProduct(newProduct); setOpen(false); + + return newProduct; } }; @@ -49,6 +50,7 @@ export default function UpdateProductItem({ newProduct.items.splice(selectedIndex!, 1); setProduct(newProduct); setOpen(false); + return newProduct; } }; diff --git a/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx b/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx index b9c43880d..b5dead984 100644 --- a/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx +++ b/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx @@ -8,10 +8,11 @@ import { import { useProductItemContext } from "../ProductItemContext"; import { useProductContext } from "../../ProductContext"; import { FeatureTypeBadge } from "@/views/features/FeatureTypeBadge"; -import { Feature } from "@autumn/shared"; +import { Feature, FeatureType, ProductItemType } from "@autumn/shared"; import { X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { PlusIcon } from "lucide-react"; +import { getItemType } from "@/utils/product/productItemUtils"; export const SelectItemFeature = ({ show, @@ -24,6 +25,8 @@ export const SelectItemFeature = ({ const { item, setItem, setShowCreateFeature, isUpdate } = useProductItemContext(); + const itemType = getItemType(item); + return (