added update product to onboarding view
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -4512,6 +4512,13 @@
|
||||
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prismjs": {
|
||||
"version": "1.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
||||
@@ -15447,6 +15454,7 @@
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
|
||||
3
server/src/external/stripe/utils.ts
vendored
3
server/src/external/stripe/utils.ts
vendored
@@ -5,6 +5,7 @@ import {
|
||||
BillingInterval,
|
||||
Feature,
|
||||
FullProduct,
|
||||
Infinite,
|
||||
Organization,
|
||||
UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
@@ -75,7 +76,7 @@ export const calculateMetered1Price = ({
|
||||
const tier = usageConfig.usage_tiers[i];
|
||||
|
||||
let amtUsed;
|
||||
if (tier.to == -1) {
|
||||
if (tier.to == -1 || tier.to == Infinite) {
|
||||
amtUsed = usage;
|
||||
} else {
|
||||
amtUsed = Math.min(usage, tier.to);
|
||||
|
||||
@@ -80,8 +80,8 @@ const createDefaultProducts = async ({
|
||||
|
||||
const defaultProducts = [
|
||||
{
|
||||
id: "free",
|
||||
name: "Free",
|
||||
id: "free-example",
|
||||
name: "Free (Example)",
|
||||
env: AppEnv.Sandbox,
|
||||
is_default: true,
|
||||
entitlements: [
|
||||
@@ -96,8 +96,8 @@ const createDefaultProducts = async ({
|
||||
prices: [],
|
||||
},
|
||||
{
|
||||
id: "pro",
|
||||
name: "Pro",
|
||||
id: "pro-example",
|
||||
name: "Pro (Example)",
|
||||
env: AppEnv.Sandbox,
|
||||
is_default: false,
|
||||
entitlements: [
|
||||
@@ -139,6 +139,7 @@ const createDefaultProducts = async ({
|
||||
is_add_on: false,
|
||||
group: "",
|
||||
created_at: Date.now(),
|
||||
version: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ import { validateMeteredConfig } from "@/internal/features/featureUtils.js";
|
||||
|
||||
export const featureApiRouter = express.Router();
|
||||
|
||||
|
||||
|
||||
export const validateFeature = (data: any) => {
|
||||
let featureType = data.type;
|
||||
|
||||
@@ -85,11 +83,13 @@ featureApiRouter.post("", async (req: any, res) => {
|
||||
...parsedFeature,
|
||||
};
|
||||
|
||||
let insertedFeature = await FeatureService.insert({
|
||||
let insertedData = await FeatureService.insert({
|
||||
sb: req.sb,
|
||||
data: feature,
|
||||
});
|
||||
|
||||
let insertedFeature =
|
||||
insertedData && insertedData.length > 0 ? insertedData[0] : null;
|
||||
res.status(200).json(insertedFeature);
|
||||
} catch (error) {
|
||||
handleRequestError({ req, error, res, action: "Create feature" });
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ErrCode, Organization, UpdateProductSchema } from "@autumn/shared";
|
||||
import {
|
||||
ErrCode,
|
||||
Organization,
|
||||
RewardProgram,
|
||||
UpdateProductSchema,
|
||||
} from "@autumn/shared";
|
||||
import { UpdateProduct } from "@autumn/shared";
|
||||
import { Product } from "@autumn/shared";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
@@ -15,12 +20,10 @@ import {
|
||||
handleVersionProduct,
|
||||
handleVersionProductV2,
|
||||
} from "./handleVersionProduct.js";
|
||||
import {
|
||||
productsAreDifferent,
|
||||
productsAreDifferent2,
|
||||
} from "@/internal/products/productUtils.js";
|
||||
import { productsAreDifferent } from "@/internal/products/productUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { handleNewProductItems } from "@/internal/products/product-items/productItemInitUtils.js";
|
||||
import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
|
||||
|
||||
export const handleUpdateProductDetails = async ({
|
||||
newProduct,
|
||||
@@ -28,12 +31,14 @@ export const handleUpdateProductDetails = async ({
|
||||
org,
|
||||
sb,
|
||||
cusProductExists,
|
||||
rewardPrograms,
|
||||
}: {
|
||||
curProduct: Product;
|
||||
newProduct: UpdateProduct;
|
||||
org: Organization;
|
||||
sb: SupabaseClient;
|
||||
cusProductExists: boolean;
|
||||
rewardPrograms: RewardProgram[];
|
||||
}) => {
|
||||
// 1. Check if they're same
|
||||
// console.log("New product: ", newProduct);
|
||||
@@ -72,12 +77,23 @@ export const handleUpdateProductDetails = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (newProduct.id !== curProduct.id && customersOnAllVersions.length > 0) {
|
||||
throw new RecaseError({
|
||||
message: "Cannot change product ID because it has existing customers",
|
||||
code: ErrCode.ProductHasCustomers,
|
||||
statusCode: 400,
|
||||
});
|
||||
if (newProduct.id !== curProduct.id) {
|
||||
if (customersOnAllVersions.length > 0) {
|
||||
throw new RecaseError({
|
||||
message: "Cannot change product ID because it has existing customers",
|
||||
code: ErrCode.ProductHasCustomers,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (rewardPrograms.length > 0) {
|
||||
throw new RecaseError({
|
||||
message:
|
||||
"Cannot change product ID because existing reward programs are linked to it",
|
||||
code: ErrCode.ProductHasRewardPrograms,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Updating product ${curProduct.id} (org: ${org.slug})`);
|
||||
@@ -146,6 +162,7 @@ export const handleUpdateProduct = async (req: any, res: any) => {
|
||||
newProduct: UpdateProductSchema.parse(req.body),
|
||||
org,
|
||||
cusProductExists,
|
||||
rewardPrograms: [],
|
||||
});
|
||||
|
||||
let productHasChanged = productsAreDifferent({
|
||||
@@ -232,7 +249,7 @@ export const handleUpdateProductV2 = async (req: any, res: any) =>
|
||||
const { productId } = req.params;
|
||||
const { sb, orgId, env, logtail: logger } = req;
|
||||
|
||||
const [features, org, fullProduct] = await Promise.all([
|
||||
const [features, org, fullProduct, rewardPrograms] = await Promise.all([
|
||||
FeatureService.getFromReq(req),
|
||||
OrgService.getFullOrg({
|
||||
sb,
|
||||
@@ -244,6 +261,12 @@ export const handleUpdateProductV2 = async (req: any, res: any) =>
|
||||
orgId,
|
||||
env,
|
||||
}),
|
||||
RewardProgramService.getByProductId({
|
||||
sb,
|
||||
productIds: [productId],
|
||||
orgId,
|
||||
env,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!fullProduct) {
|
||||
@@ -255,6 +278,8 @@ export const handleUpdateProductV2 = async (req: any, res: any) =>
|
||||
}
|
||||
|
||||
// 1. Update product details
|
||||
// Get reward programs using product id
|
||||
|
||||
const cusProductsCurVersion =
|
||||
await CusProductService.getByInternalProductId(
|
||||
sb,
|
||||
@@ -269,6 +294,7 @@ export const handleUpdateProductV2 = async (req: any, res: any) =>
|
||||
newProduct: UpdateProductSchema.parse(req.body),
|
||||
org,
|
||||
cusProductExists,
|
||||
rewardPrograms,
|
||||
});
|
||||
|
||||
// 1. Map to product items
|
||||
|
||||
@@ -164,6 +164,7 @@ export const handleVersionProductV2 = async ({
|
||||
// Validate product items...
|
||||
validateProductItems({
|
||||
newItems: items,
|
||||
features,
|
||||
});
|
||||
|
||||
await ProductService.create({ sb, product: newProduct });
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
|
||||
import { Router } from "express";
|
||||
import {
|
||||
AppEnv,
|
||||
CreateFeatureSchema,
|
||||
CreateProductSchema,
|
||||
} from "@autumn/shared";
|
||||
import { CreateProductSchema } from "@autumn/shared";
|
||||
|
||||
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import { ErrCode } from "@/errors/errCodes.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { initNewFeature } from "../features/featureApiRouter.js";
|
||||
|
||||
import {
|
||||
checkStripeProductExists,
|
||||
constructProduct,
|
||||
copyProduct,
|
||||
} from "@/internal/products/productUtils.js";
|
||||
import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
|
||||
import {
|
||||
handleUpdateProduct,
|
||||
handleUpdateProductV2,
|
||||
} from "./handleUpdateProduct.js";
|
||||
import { handleUpdateProductV2 } from "./handleUpdateProduct.js";
|
||||
import { handleDeleteProduct } from "./handleDeleteProduct.js";
|
||||
import { handleGetProduct } from "./handleGetProduct.js";
|
||||
import { handleCopyProduct } from "./handlers/handleCopyProduct.js";
|
||||
|
||||
@@ -52,7 +52,7 @@ export const getCusBalances = async ({
|
||||
if (!data[key]) {
|
||||
data[key] = {
|
||||
feature_id: feature.id,
|
||||
interval: ent.interval || undefined,
|
||||
interval: isBoolean || unlimited ? null : ent.interval || undefined,
|
||||
unlimited: isBoolean ? undefined : unlimited,
|
||||
balance: isBoolean ? undefined : unlimited ? null : 0,
|
||||
total: isBoolean || unlimited ? undefined : 0,
|
||||
|
||||
@@ -29,7 +29,8 @@ export class FeatureService {
|
||||
.select("*")
|
||||
.eq("org_id", orgId)
|
||||
.eq("env", env)
|
||||
.order("created_at", { ascending: false }).order("id");
|
||||
.order("created_at", { ascending: false })
|
||||
.order("id");
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
@@ -138,8 +139,7 @@ export class FeatureService {
|
||||
let { data: insertedData, error } = await sb
|
||||
.from("features")
|
||||
.insert(data)
|
||||
.select()
|
||||
.single();
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
if (error.code === "23505") {
|
||||
|
||||
@@ -46,7 +46,7 @@ orgRouter.post("/stripe", async (req: any, res) => {
|
||||
try {
|
||||
let { testApiKey, liveApiKey, successUrl, defaultCurrency } = req.body;
|
||||
|
||||
if (!testApiKey || !liveApiKey || !defaultCurrency || !successUrl) {
|
||||
if (!testApiKey || !liveApiKey || !successUrl) {
|
||||
throw new RecaseError({
|
||||
message: "Missing required fields",
|
||||
code: ErrCode.StripeKeyInvalid,
|
||||
|
||||
@@ -120,7 +120,10 @@ export const validateEntitlement = ({
|
||||
}
|
||||
|
||||
if (parsedEnt.allowance_type == AllowanceType.Fixed) {
|
||||
if (!notNullOrUndefined(parsedEnt.allowance) || parsedEnt.allowance! < 0) {
|
||||
if (
|
||||
!notNullOrUndefined(parsedEnt.allowance) ||
|
||||
(typeof parsedEnt.allowance === "number" && parsedEnt.allowance < 0)
|
||||
) {
|
||||
throw new RecaseError({
|
||||
code: ErrCode.InvalidEntitlement,
|
||||
message: `Allowance is required for feature ${parsedEnt.feature_id}`,
|
||||
@@ -156,9 +159,13 @@ export const validateEntitlement = ({
|
||||
}
|
||||
|
||||
let billingUnits = config.billing_units || 1;
|
||||
let isMultipleOfBillingUnits = parsedEnt.allowance! % billingUnits === 0;
|
||||
let isMultipleOfBillingUnits =
|
||||
(parsedEnt.allowance! as number) % billingUnits === 0;
|
||||
|
||||
if (parsedEnt.allowance! < billingUnits || !isMultipleOfBillingUnits) {
|
||||
if (
|
||||
(parsedEnt.allowance! as number) < billingUnits ||
|
||||
!isMultipleOfBillingUnits
|
||||
) {
|
||||
throw new RecaseError({
|
||||
code: ErrCode.InvalidEntitlement,
|
||||
message: `Allowance for ${parsedEnt.feature_id} must be ≥ billing units and a multiple of billing units`,
|
||||
|
||||
@@ -14,10 +14,15 @@ export const billingToItemInterval = (billingInterval: BillingInterval) => {
|
||||
return billingInterval as unknown as ProductItemInterval;
|
||||
};
|
||||
|
||||
export const entToItemInterval = (entInterval: EntInterval) => {
|
||||
export const entToItemInterval = (entInterval?: EntInterval) => {
|
||||
if (nullish(entInterval)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (entInterval == EntInterval.Lifetime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return entInterval as unknown as ProductItemInterval;
|
||||
};
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ export const toFeature = ({
|
||||
internalProductId,
|
||||
isCustom,
|
||||
newVersion,
|
||||
feature,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
orgId: string;
|
||||
@@ -87,7 +88,10 @@ export const toFeature = ({
|
||||
internalProductId: string;
|
||||
isCustom: boolean;
|
||||
newVersion?: boolean;
|
||||
feature?: Feature;
|
||||
}) => {
|
||||
let isBoolean = feature?.type == FeatureType.Boolean;
|
||||
|
||||
let ent: Entitlement = {
|
||||
id: item.entitlement_id || generateId("ent"),
|
||||
org_id: orgId,
|
||||
@@ -103,10 +107,11 @@ export const toFeature = ({
|
||||
item.included_usage == Infinite
|
||||
? AllowanceType.Unlimited
|
||||
: AllowanceType.Fixed,
|
||||
interval:
|
||||
item.reset_usage_on_billing === false
|
||||
? EntInterval.Lifetime
|
||||
: (itemToEntInterval(item) as EntInterval),
|
||||
interval: isBoolean
|
||||
? null
|
||||
: item.reset_usage_on_billing === false
|
||||
? EntInterval.Lifetime
|
||||
: (itemToEntInterval(item) as EntInterval),
|
||||
|
||||
carry_from_previous: item.carry_over_usage || false,
|
||||
entity_feature_id: item.entity_feature_id,
|
||||
@@ -308,6 +313,7 @@ export const itemToPriceAndEnt = ({
|
||||
internalProductId,
|
||||
isCustom,
|
||||
newVersion,
|
||||
feature,
|
||||
});
|
||||
|
||||
if (!curEnt || newVersion) {
|
||||
|
||||
@@ -37,6 +37,7 @@ export const toFeatureItem = ({ ent }: { ent: EntitlementWithFeature }) => {
|
||||
if (ent.feature.type == FeatureType.Boolean) {
|
||||
return {
|
||||
feature_id: ent.feature.id,
|
||||
entitlement_id: ent.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ export const handleNewProductItems = async ({
|
||||
// Validate product items...
|
||||
validateProductItems({
|
||||
newItems,
|
||||
features,
|
||||
});
|
||||
|
||||
let newPrices: Price[] = [];
|
||||
|
||||
@@ -84,13 +84,13 @@ export const constructFeatureItem = ({
|
||||
entitlement_id,
|
||||
}: {
|
||||
feature_id: string;
|
||||
included_usage?: number | typeof Infinite;
|
||||
interval: EntInterval;
|
||||
included_usage?: number | string;
|
||||
interval?: EntInterval;
|
||||
entitlement_id?: string;
|
||||
}) => {
|
||||
let item: ProductItem = {
|
||||
feature_id,
|
||||
included_usage,
|
||||
included_usage: included_usage as number,
|
||||
interval: entToItemInterval(interval),
|
||||
entitlement_id,
|
||||
};
|
||||
|
||||
@@ -7,14 +7,23 @@ import {
|
||||
ProductItemSchema,
|
||||
Infinite,
|
||||
ProductItemInterval,
|
||||
Feature,
|
||||
FeatureType,
|
||||
} from "@autumn/shared";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { isFeaturePriceItem } from "./productItemUtils.js";
|
||||
import { notNullish, nullish } from "@/utils/genUtils.js";
|
||||
import { isFeatureItem, isPriceItem } from "./getItemType.js";
|
||||
import { itemToEntInterval } from "./itemIntervalUtils.js";
|
||||
const validateProductItem = ({ item }: { item: ProductItem }) => {
|
||||
const validateProductItem = ({
|
||||
item,
|
||||
features,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
features: Feature[];
|
||||
}) => {
|
||||
item = ProductItemSchema.parse(item);
|
||||
|
||||
// 1. Check if amount and tiers are not null
|
||||
if (notNullish(item.amount) && notNullish(item.tiers)) {
|
||||
throw new RecaseError({
|
||||
@@ -101,12 +110,14 @@ const validateProductItem = ({ item }: { item: ProductItem }) => {
|
||||
};
|
||||
export const validateProductItems = ({
|
||||
newItems,
|
||||
features,
|
||||
}: {
|
||||
newItems: ProductItem[];
|
||||
features: Feature[];
|
||||
}) => {
|
||||
// 1. Check values
|
||||
for (let index = 0; index < newItems.length; index++) {
|
||||
validateProductItem({ item: newItems[index] });
|
||||
validateProductItem({ item: newItems[index], features });
|
||||
}
|
||||
|
||||
for (let index = 0; index < newItems.length; index++) {
|
||||
|
||||
@@ -14,19 +14,19 @@ export const publicAttachRouter = Router();
|
||||
|
||||
export const handlePublicAttach = async (req: any, res: any) => {
|
||||
{
|
||||
const { customer_id, product_id, success_url, options } = req.body;
|
||||
const orgId = req.minOrg.id;
|
||||
const env = req.env;
|
||||
|
||||
const sb = req.sb;
|
||||
|
||||
const useCheckout = true;
|
||||
const optionsListInput = options || [];
|
||||
console.log("--------------------------------");
|
||||
console.log(`PUBLIC ATTACH PRODUCT REQUEST (from ${req.minOrg.slug})`);
|
||||
|
||||
try {
|
||||
// 1. Get full customer product data
|
||||
const { customer_id, product_id, success_url, options } = req.body;
|
||||
const orgId = req.minOrg.id;
|
||||
const env = req.env;
|
||||
|
||||
const sb = req.sb;
|
||||
|
||||
const useCheckout = true;
|
||||
const optionsListInput = options || [];
|
||||
console.log("--------------------------------");
|
||||
console.log(`PUBLIC ATTACH PRODUCT REQUEST (from ${req.minOrg.slug})`);
|
||||
|
||||
const attachParams: AttachParams = await getFullCusProductData({
|
||||
sb,
|
||||
customerId: customer_id,
|
||||
@@ -34,8 +34,7 @@ export const handlePublicAttach = async (req: any, res: any) => {
|
||||
orgId: orgId,
|
||||
env,
|
||||
customerData: {} as any,
|
||||
pricesInput: [],
|
||||
entsInput: [],
|
||||
itemsInput: [],
|
||||
optionsListInput: optionsListInput,
|
||||
freeTrialInput: null,
|
||||
isCustom: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { ErrCode, RewardProgram } from "@autumn/shared";
|
||||
import { ErrCode, RewardProgram, RewardTriggerEvent } from "@autumn/shared";
|
||||
import { ReferralCode } from "@shared/models/rewardModels/referralModels/referralModels.js";
|
||||
|
||||
export class RewardProgramService {
|
||||
@@ -76,6 +76,32 @@ export class RewardProgramService {
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getByProductId({
|
||||
sb,
|
||||
productIds,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
sb: any;
|
||||
productIds: string[];
|
||||
orgId: string;
|
||||
env: string;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("reward_programs")
|
||||
.select("*")
|
||||
.eq("org_id", orgId)
|
||||
.eq("env", env)
|
||||
.eq("when", RewardTriggerEvent.Checkout)
|
||||
.contains("product_ids", productIds);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async create({
|
||||
sb,
|
||||
data,
|
||||
|
||||
@@ -4,10 +4,8 @@ import http from "http";
|
||||
import { createSupabaseClient } from "@/external/supabaseUtils.js";
|
||||
import { AppEnv, ErrCode } from "@autumn/shared";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import {
|
||||
getBalanceForFeature,
|
||||
getCusBalances,
|
||||
} from "@/internal/customers/entitlements/cusEntUtils.js";
|
||||
import { getBalanceForFeature } from "@/internal/customers/entitlements/cusEntUtils.js";
|
||||
import { getCusBalances } from "@/internal/customers/entitlements/getCusBalances.js";
|
||||
|
||||
export enum SbChannelEvent {
|
||||
BalanceUpdated = "balance_updated",
|
||||
@@ -145,52 +143,47 @@ export const initWs = (server: http.Server) => {
|
||||
};
|
||||
|
||||
const handleRealtimeBalances = async (ws: WebSocket, req: any, params: any) => {
|
||||
try {
|
||||
const { org, env, sb } = req;
|
||||
|
||||
// 1. Get all customer balances
|
||||
const balances = await getCusBalances({
|
||||
sb,
|
||||
customerId: params.customer_id,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
ws.send(JSON.stringify({ data: balances, error: null }));
|
||||
|
||||
const channel = `${org.id}_${env}_${params.customer_id}`;
|
||||
|
||||
sb.channel(channel)
|
||||
.on(
|
||||
"broadcast",
|
||||
{ event: SbChannelEvent.BalanceUpdated },
|
||||
async (payload: any) => {
|
||||
const data = payload.payload;
|
||||
console.log("Received balance update event from supabase:", data);
|
||||
const newBalances = await getCusBalances({
|
||||
sb,
|
||||
customerId: params.customer_id,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
data: newBalances,
|
||||
error: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
} catch (error) {
|
||||
console.log("Error getting customer balances", error);
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
data: null,
|
||||
error: "Error getting customer balances",
|
||||
})
|
||||
);
|
||||
}
|
||||
// try {
|
||||
// const { org, env, sb } = req;
|
||||
// // 1. Get all customer balances
|
||||
// const balances = await getCusBalances({
|
||||
// sb,
|
||||
// customerId: params.customer_id,
|
||||
// orgId: org.id,
|
||||
// env,
|
||||
// });
|
||||
// ws.send(JSON.stringify({ data: balances, error: null }));
|
||||
// const channel = `${org.id}_${env}_${params.customer_id}`;
|
||||
// sb.channel(channel)
|
||||
// .on(
|
||||
// "broadcast",
|
||||
// { event: SbChannelEvent.BalanceUpdated },
|
||||
// async (payload: any) => {
|
||||
// const data = payload.payload;
|
||||
// console.log("Received balance update event from supabase:", data);
|
||||
// const newBalances = await getCusBalances({
|
||||
// customerId: params.customer_id,
|
||||
// orgId: org.id,
|
||||
// env,
|
||||
// });
|
||||
// ws.send(
|
||||
// JSON.stringify({
|
||||
// data: newBalances,
|
||||
// error: null,
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
// )
|
||||
// .subscribe();
|
||||
// } catch (error) {
|
||||
// console.log("Error getting customer balances", error);
|
||||
// ws.send(
|
||||
// JSON.stringify({
|
||||
// data: null,
|
||||
// error: "Error getting customer balances",
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
};
|
||||
|
||||
const handleRealtimeBalance = async (ws: WebSocket, req: any, params: any) => {
|
||||
|
||||
@@ -5,17 +5,17 @@ MOCHA_CMD="npx mocha --parallel --timeout 10000000 --ignore tests/00_setup.ts"
|
||||
# TEST PARALLEL
|
||||
if [ "$1" == "basic-parallel" ]; then
|
||||
MOCHA_PARALLEL=true $MOCHA_SETUP && $MOCHA_CMD \
|
||||
tests/attach/**/*.ts \
|
||||
# tests/basic/*.ts \
|
||||
# tests/basic/entities/*.ts \
|
||||
tests/basic/*.ts \
|
||||
tests/basic/multi-feature/*.ts \
|
||||
tests/basic/entities/*.ts \
|
||||
# tests/basic/referrals/*.ts \
|
||||
# tests/basic/multi-feature/*.ts \
|
||||
# tests/attach/**/*.ts \
|
||||
|
||||
elif [ "$1" == "advanced-parallel" ]; then
|
||||
MOCHA_PARALLEL=true \
|
||||
$MOCHA_SETUP \
|
||||
&& $MOCHA_CMD 'tests/advanced/arrear_prorated/*.ts'\
|
||||
# && $MOCHA_CMD 'tests/advanced/usage/*.ts'\
|
||||
&& $MOCHA_CMD 'tests/advanced/usage/*.ts' \
|
||||
&& $MOCHA_CMD 'tests/advanced/arrear_prorated/*.ts' 'tests/advanced/coupons/*.ts'\
|
||||
# && $MOCHA_CMD 'tests/advanced/coupons/*.ts'\
|
||||
|
||||
|
||||
|
||||
@@ -91,12 +91,20 @@ export const checkFeatureHasCorrectBalance = async ({
|
||||
// Get ent from cusRes
|
||||
const { entitlements: cusEnts }: any = cusRes;
|
||||
const { allowed, balanceObj }: any = entitledRes;
|
||||
|
||||
const cusEnt = cusEnts.find(
|
||||
(e: any) =>
|
||||
e.feature_id === feature.id && e.interval == entitlement.interval
|
||||
);
|
||||
|
||||
expect(cusEnt).to.exist;
|
||||
try {
|
||||
expect(cusEnt).to.exist;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`Expected cus ent ${feature.id}, interval ${entitlement.interval} to exist`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (entitlement.allowance_type === AllowanceType.Unlimited) {
|
||||
// Cus ent
|
||||
|
||||
@@ -32,7 +32,7 @@ export const getCusProduct = async (
|
||||
return data[0];
|
||||
};
|
||||
|
||||
describe.skip(`${chalk.yellowBright(
|
||||
describe(`${chalk.yellowBright(
|
||||
"07_downgrade: testing downgrade (paid to paid)"
|
||||
)}`, () => {
|
||||
let customer: Customer;
|
||||
|
||||
@@ -80,6 +80,7 @@ export const ErrCode = {
|
||||
DefaultProductNotAllowedPrice: "default_product_not_allowed_price",
|
||||
UpgradeFailed: "upgrade_failed",
|
||||
ProductAlreadyExists: "product_already_exists",
|
||||
ProductHasRewardPrograms: "product_has_reward_programs",
|
||||
|
||||
// Entitlements
|
||||
InvalidEntitlement: "invalid_entitlement",
|
||||
|
||||
@@ -33,7 +33,7 @@ export const CreateEntitlementSchema = z.object({
|
||||
internal_feature_id: z.string(),
|
||||
feature_id: z.string(),
|
||||
allowance_type: z.nativeEnum(AllowanceType).nullish(),
|
||||
allowance: z.union([z.number(), z.literal("unlimited")]).nullish(),
|
||||
allowance: z.number().nullish(),
|
||||
interval: z.nativeEnum(EntInterval).nullish(),
|
||||
carry_from_previous: z.boolean().default(false),
|
||||
entity_feature_id: z.string().nullish(),
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
|
||||
@@ -76,7 +76,12 @@ export function MainLayout() {
|
||||
}
|
||||
|
||||
if (!org && !pathname.includes("/onboarding")) {
|
||||
return <Navigate to={getRedirectUrl("/onboarding", env)} replace={true} />;
|
||||
return (
|
||||
<Navigate
|
||||
to={getRedirectUrl("/onboarding", AppEnv.Sandbox)}
|
||||
replace={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 3. If user, but no org, redirect to onboarding
|
||||
|
||||
@@ -18,7 +18,8 @@ export const AdminHover = ({
|
||||
let isAdmin =
|
||||
email === "johnyeocx@gmail.com" ||
|
||||
email === "ayush@recaseai.com" ||
|
||||
email === "johnyeo10@gmail.com";
|
||||
email === "johnyeo10@gmail.com" ||
|
||||
email == "npmrundemo@gmail.com";
|
||||
|
||||
if (!isAdmin) return children;
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
props.onClick?.(e);
|
||||
}}
|
||||
>
|
||||
{isLoading && <LoaderCircle className="animate-spin" size={20} />}
|
||||
{isLoading && <LoaderCircle className="animate-spin" size={17} />}
|
||||
{startIcon && !isLoading && <>{startIcon}</>}
|
||||
{variant == "add" && <PlusIcon size={12} />}
|
||||
{children}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
@@ -49,8 +49,6 @@ import CheckAccessStep from "./onboarding-steps/05_CheckAccess";
|
||||
|
||||
function OnboardingView() {
|
||||
const env = useEnv();
|
||||
const { user } = useUser();
|
||||
// Started without org...
|
||||
|
||||
const { organization: org } = useOrganization();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -58,6 +56,7 @@ function OnboardingView() {
|
||||
|
||||
let [apiKey, setApiKey] = useState("");
|
||||
let [productId, setProductId] = useState("");
|
||||
const hasHandledOrg = useRef(false);
|
||||
|
||||
const axiosInstance = useAxiosInstance({ env });
|
||||
|
||||
@@ -102,7 +101,9 @@ function OnboardingView() {
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (org && !orgCreated) {
|
||||
if (org && !orgCreated && !hasHandledOrg.current) {
|
||||
// console.log("Gonna poll for org!");
|
||||
hasHandledOrg.current = true;
|
||||
pollForOrg();
|
||||
}
|
||||
}, [org, orgCreated]);
|
||||
@@ -110,10 +111,9 @@ function OnboardingView() {
|
||||
return (
|
||||
<div className="text-sm w-full flex justify-start">
|
||||
<div className="flex flex-col p-8 px-14">
|
||||
<CreateOrgStep pollForOrg={pollForOrg} number={1} />
|
||||
{orgCreated && (
|
||||
<>
|
||||
<CreateOrgStep pollForOrg={pollForOrg} number={1} />
|
||||
|
||||
<CreateProductStep
|
||||
productId={productId}
|
||||
setProductId={setProductId}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function CopyButton({ content }: { content: string }) {
|
||||
|
||||
return (
|
||||
<button
|
||||
className="p-2.5 text-white/60 hover:text-foreground/80 transition-colors"
|
||||
className="p-2.5 text-white/60 hover:text-white/80 transition-colors"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
|
||||
@@ -38,7 +38,7 @@ export const CreateOrgStep = ({
|
||||
});
|
||||
|
||||
await setActive({ organization: org.id });
|
||||
await pollForOrg();
|
||||
// await pollForOrg();
|
||||
toast.success(`Created your organization: ${org.name}`);
|
||||
setIsExploding(true);
|
||||
} catch (error: any) {
|
||||
@@ -74,9 +74,10 @@ export const CreateOrgStep = ({
|
||||
}
|
||||
>
|
||||
{/* <div className="flex gap-8 w-full justify-between flex-col lg:flex-row"> */}
|
||||
<div className="w-full min-w-md max-w-2xl flex gap-2 rounded-sm">
|
||||
<div className="w-full min-w-md flex gap-2">
|
||||
<Input
|
||||
placeholder="Org name"
|
||||
className="w-full"
|
||||
value={org?.name || fields.name}
|
||||
disabled={!!org?.name}
|
||||
onChange={(e) => {
|
||||
@@ -85,7 +86,7 @@ export const CreateOrgStep = ({
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="min-w-40 w-40 max-w-40"
|
||||
className="min-w-44 w-44 max-w-44"
|
||||
disabled={!!org?.name}
|
||||
onClick={handleCreateOrg}
|
||||
isLoading={loading}
|
||||
@@ -97,6 +98,7 @@ export const CreateOrgStep = ({
|
||||
|
||||
{isExploding && (
|
||||
<ConfettiExplosion
|
||||
className="absolute"
|
||||
force={0.8}
|
||||
duration={3000}
|
||||
particleCount={250}
|
||||
|
||||
@@ -84,7 +84,7 @@ export const ConnectStripeStep = ({
|
||||
/> */}
|
||||
<Button
|
||||
variant="gradientPrimary"
|
||||
className="min-w-40"
|
||||
className="min-w-44 w-44 max-w-44"
|
||||
onClick={handleConnectStripe}
|
||||
isLoading={loading}
|
||||
disabled={stripeConnected}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import Step from "@/components/general/OnboardingStep";
|
||||
import { FeaturesContext } from "@/views/features/FeaturesContext";
|
||||
import { ProductsTable } from "@/views/products/ProductsTable";
|
||||
import SmallSpinner from "@/components/general/SmallSpinner";
|
||||
import { ProductsContext } from "@/views/products/ProductsContext";
|
||||
import CreateProduct, { defaultProduct } from "@/views/products/CreateProduct";
|
||||
import { ProductConfig } from "@/views/products/ProductConfig";
|
||||
import { Product } from "@autumn/shared";
|
||||
|
||||
import { defaultProduct } from "@/views/products/CreateProduct";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAxiosSWR } from "@/services/useAxiosSwr";
|
||||
@@ -17,7 +13,7 @@ import { toast } from "sonner";
|
||||
import { ProductContext } from "@/views/products/product/ProductContext";
|
||||
import { ManageProduct } from "@/views/products/product/ManageProduct";
|
||||
import { ProductItemTable } from "@/views/products/product/product-item/ProductItemTable";
|
||||
import { CopyIcon, PlusIcon } from "lucide-react";
|
||||
import { ArrowUp, CopyIcon, PlusIcon } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { slugify } from "@/utils/formatUtils/formatTextUtils";
|
||||
|
||||
@@ -58,6 +54,22 @@ export const CreateProductStep = ({
|
||||
setCreateProductLoading(false);
|
||||
};
|
||||
|
||||
const updateProduct = async () => {
|
||||
setCreateProductLoading(true);
|
||||
try {
|
||||
const res = await ProductService.updateProduct(
|
||||
axiosInstance,
|
||||
productId,
|
||||
product
|
||||
);
|
||||
toast.success("Product items successfully created");
|
||||
await mutate();
|
||||
} catch (error) {
|
||||
toast.error(getBackendErr(error, "Failed to update product"));
|
||||
}
|
||||
setCreateProductLoading(false);
|
||||
};
|
||||
|
||||
const [product, setProduct] = useState<any>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -100,6 +112,16 @@ export const CreateProductStep = ({
|
||||
}}
|
||||
>
|
||||
<ProductItemTable isOnboarding={true} />
|
||||
<div className="flex justify-end mt-4">
|
||||
<Button
|
||||
isLoading={createProductLoading}
|
||||
variant="gradientPrimary"
|
||||
onClick={updateProduct}
|
||||
className="min-w-44 w-44 max-w-44"
|
||||
>
|
||||
Update Product
|
||||
</Button>
|
||||
</div>
|
||||
</ProductContext.Provider>
|
||||
</FeaturesContext.Provider>
|
||||
) : (
|
||||
@@ -126,7 +148,7 @@ const CreateProductCard = ({
|
||||
createProductLoading: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className=" flex gap-2 items-start">
|
||||
<div className="flex gap-2 items-start">
|
||||
{/* <ProductConfig
|
||||
product={newProduct}
|
||||
setProduct={setNewProduct}
|
||||
@@ -151,7 +173,7 @@ const CreateProductCard = ({
|
||||
|
||||
<Button
|
||||
variant="gradientPrimary"
|
||||
className="min-w-40"
|
||||
className="min-w-44 w-44 max-w-44"
|
||||
onClick={createProduct}
|
||||
isLoading={createProductLoading}
|
||||
// startIcon={<PlusIcon size={15} />}
|
||||
|
||||
@@ -115,7 +115,7 @@ export const CreateSecretKey = ({
|
||||
onClick={handleCreate}
|
||||
isLoading={loading}
|
||||
variant="gradientPrimary"
|
||||
className="min-w-40"
|
||||
className="min-w-44 w-44 max-w-44"
|
||||
>
|
||||
Create Secret Key
|
||||
</Button>
|
||||
|
||||
@@ -94,9 +94,6 @@ function ProductView({ env }: { env: AppEnv }) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Original product:", originalProduct.items);
|
||||
console.log("Current product:", product.items);
|
||||
|
||||
const hasChanged =
|
||||
JSON.stringify(sortedProduct) !== JSON.stringify(originalProduct);
|
||||
setHasChanges(hasChanged);
|
||||
|
||||
@@ -45,7 +45,7 @@ function CreateFixedPrice({
|
||||
className="h-full !text-lg min-w-36"
|
||||
/>
|
||||
<span className="text-t2 w-fit px-6 flex justify-center">
|
||||
{org?.default_currency}
|
||||
{org?.default_currency.toUpperCase() || "USD"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,7 +99,7 @@ export const PricingConfig = ({
|
||||
<TabsTrigger value={PriceType.Usage}>Usage Based</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value={PriceType.Fixed}> */}
|
||||
<CreateFixedPrice config={fixedConfig} setConfig={setFixedConfig} />
|
||||
{/* <CreateFixedPrice config={fixedConfig} setConfig={setFixedConfig} /> */}
|
||||
{/* </TabsContent>
|
||||
<TabsContent value={PriceType.Usage}>
|
||||
<CreateUsagePrice
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Feature,
|
||||
FeatureType,
|
||||
Infinite,
|
||||
ProductItemInterval,
|
||||
TierInfinite,
|
||||
} from "@autumn/shared";
|
||||
@@ -37,7 +38,6 @@ export const ProductItemConfig = () => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(item, "item");
|
||||
// if show price is changed to false, remove the "amount" from the item
|
||||
if (show.price) {
|
||||
setItem({ ...item, amount: null });
|
||||
@@ -138,7 +138,7 @@ export const ProductItemConfig = () => {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleAddPrice}
|
||||
disabled={item.included_usage == "unlimited"}
|
||||
disabled={item.included_usage == Infinite}
|
||||
className={cn(
|
||||
"w-0 max-w-0 p-0 overflow-hidden transition-all duration-200 ease-in-out",
|
||||
!show.price &&
|
||||
@@ -228,96 +228,3 @@ export const ProductItemConfig = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// const [originalEntitlement, _] = useState<Entitlement | null>(
|
||||
// entitlement || null
|
||||
// );
|
||||
// const [showPerEntity, setShowPerEntity] = useState(
|
||||
// entitlement?.entity_feature_id ? true : false
|
||||
// );
|
||||
|
||||
// const [showPrice, setShowPrice] = useState(
|
||||
// priceConfig.usage_tiers?.[0].amount > 0 ||
|
||||
// priceConfig.usage_tiers?.length > 1 ||
|
||||
// priceConfig.usage_tiers?.[0].to == -1 || // to prevent for a weird state with 0 price
|
||||
// priceConfig.type == PriceType.Fixed ||
|
||||
// buttonType == "price"
|
||||
// ); // for the add price button
|
||||
|
||||
// const [showCycle, setShowCycle] = useState(
|
||||
// entitlement && entitlement?.interval == EntInterval.Lifetime ? false : true
|
||||
// );
|
||||
|
||||
// const [fields, setFields] = useState({
|
||||
// carry_from_previous: entitlement?.carry_from_previous || false,
|
||||
// allowance_type: entitlement?.allowance_type || AllowanceType.Fixed,
|
||||
// allowance: entitlement?.allowance || "",
|
||||
// interval: entitlement?.interval || EntInterval.Month,
|
||||
// entity_feature_id: entitlement?.entity_feature_id || "",
|
||||
// });
|
||||
|
||||
// useEffect(() => {
|
||||
// //translate pricing usage tiers into entitlement allowance config when saving new feature
|
||||
// console.log(selectedFeature?.name, "priceConfig:", priceConfig);
|
||||
|
||||
// let newAllowance: number | "unlimited";
|
||||
// if (fields.allowance_type == AllowanceType.Unlimited) {
|
||||
// newAllowance = "unlimited";
|
||||
// } else if (
|
||||
// priceConfig.usage_tiers?.[0].amount == 0 &&
|
||||
// priceConfig.usage_tiers?.[0].to > 0 // to prevent for a weird bug with 0 price
|
||||
// ) {
|
||||
// newAllowance = Number(priceConfig.usage_tiers?.[0].to);
|
||||
// if (isNaN(newAllowance)) {
|
||||
// newAllowance = 0;
|
||||
// }
|
||||
// } else {
|
||||
// newAllowance = 0;
|
||||
// }
|
||||
|
||||
// let newEntInterval;
|
||||
// if (showPrice && showCycle) {
|
||||
// newEntInterval =
|
||||
// priceConfig.interval == BillingInterval.OneOff
|
||||
// ? EntInterval.Lifetime
|
||||
// : fields.interval;
|
||||
// } else if (showCycle) {
|
||||
// newEntInterval = fields.interval;
|
||||
// } else {
|
||||
// newEntInterval = EntInterval.Lifetime;
|
||||
// }
|
||||
|
||||
// if (selectedFeature) {
|
||||
// const newEnt = CreateEntitlementSchema.parse({
|
||||
// internal_feature_id: selectedFeature.internal_id,
|
||||
// feature_id: selectedFeature.id,
|
||||
// feature: selectedFeature,
|
||||
// ...fields,
|
||||
// interval: newEntInterval,
|
||||
// entity_feature_id:
|
||||
// fields.entity_feature_id && showPerEntity
|
||||
// ? fields.entity_feature_id
|
||||
// : null,
|
||||
// // allowance: fields.allowance ? Number(fields.allowance) : 0,
|
||||
// allowance: newAllowance,
|
||||
// });
|
||||
|
||||
// const originalEnt = originalEntitlement ? originalEntitlement : null;
|
||||
// setEntitlement({
|
||||
// ...originalEnt,
|
||||
// ...newEnt,
|
||||
// feature: selectedFeature,
|
||||
// } as EntitlementWithFeature);
|
||||
// } else {
|
||||
// setEntitlement(null);
|
||||
// }
|
||||
// }, [
|
||||
// selectedFeature,
|
||||
// showCycle,
|
||||
// showPrice,
|
||||
// priceConfig,
|
||||
// fields,
|
||||
// originalEntitlement,
|
||||
// showPerEntity,
|
||||
// setEntitlement,
|
||||
// ]);
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Button } from "@/components/ui/button";
|
||||
import TieredPrice from "../TieredPrice";
|
||||
|
||||
import { useProductContext } from "../../ProductContext";
|
||||
import { Feature, FeatureType } from "@autumn/shared";
|
||||
import { Feature, FeatureType, Infinite } from "@autumn/shared";
|
||||
import { itemIsUnlimited } from "@/utils/product/productItemUtils";
|
||||
import { SelectCycle } from "./SelectCycle";
|
||||
import MoreMenuButton from "../MoreMenuButton";
|
||||
@@ -92,12 +92,14 @@ export const ConfigWithFeature = ({
|
||||
<Input
|
||||
placeholder="None"
|
||||
className=""
|
||||
disabled={item.included_usage == "unlimited"}
|
||||
value={item.included_usage}
|
||||
disabled={item.included_usage == Infinite}
|
||||
value={
|
||||
item.included_usage == Infinite
|
||||
? "Unlimited"
|
||||
: item.included_usage
|
||||
}
|
||||
type={
|
||||
item.included_usage === "unlimited"
|
||||
? "text"
|
||||
: "number"
|
||||
item.included_usage === Infinite ? "text" : "number"
|
||||
}
|
||||
onChange={(e) => {
|
||||
setItem({
|
||||
@@ -108,7 +110,7 @@ export const ConfigWithFeature = ({
|
||||
/>
|
||||
<ToggleDisplayButton
|
||||
label="Unlimited"
|
||||
show={item.included_usage == "unlimited"}
|
||||
show={item.included_usage == Infinite}
|
||||
className="h-8"
|
||||
onClick={() => {
|
||||
setShow({ ...show, price: false });
|
||||
@@ -120,7 +122,7 @@ export const ConfigWithFeature = ({
|
||||
} else {
|
||||
setItem({
|
||||
...item,
|
||||
included_usage: "unlimited",
|
||||
included_usage: Infinite,
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { TooltipContent } from "@/components/ui/tooltip";
|
||||
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { BillingInterval, EntInterval } from "@autumn/shared";
|
||||
import { BillingInterval, EntInterval, Infinite } from "@autumn/shared";
|
||||
import { InfoIcon, X } from "lucide-react";
|
||||
import { useProductItemContext } from "../ProductItemContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -85,7 +85,7 @@ export const SelectCycle = ({
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Select
|
||||
disabled={item.included_usage == "unlimited"}
|
||||
disabled={item.included_usage == Infinite}
|
||||
value={itemToEntInterval(item) as string}
|
||||
onValueChange={(value) => {
|
||||
setItem({
|
||||
|
||||
Reference in New Issue
Block a user