added update product to onboarding view

This commit is contained in:
John Yeo
2025-04-15 22:31:17 +01:00
parent f27f8c9df1
commit 9ae0b278e8
41 changed files with 280 additions and 257 deletions

8
package-lock.json generated
View File

@@ -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",

View File

@@ -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);

View File

@@ -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,
}),
});

View File

@@ -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" });

View File

@@ -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

View File

@@ -164,6 +164,7 @@ export const handleVersionProductV2 = async ({
// Validate product items...
validateProductItems({
newItems: items,
features,
});
await ProductService.create({ sb, product: newProduct });

View File

@@ -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";

View File

@@ -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,

View File

@@ -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") {

View File

@@ -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,

View File

@@ -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`,

View File

@@ -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;
};

View File

@@ -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) {

View File

@@ -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,
};
}

View File

@@ -140,6 +140,7 @@ export const handleNewProductItems = async ({
// Validate product items...
validateProductItems({
newItems,
features,
});
let newPrices: Price[] = [];

View File

@@ -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,
};

View File

@@ -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++) {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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) => {

View File

@@ -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'\

View File

@@ -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

View File

@@ -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;

View File

@@ -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",

View File

@@ -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(),

View File

@@ -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",

View File

@@ -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

View File

@@ -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;

View File

@@ -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}

View File

@@ -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}

View File

@@ -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" />}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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} />}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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

View File

@@ -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,
// ]);

View File

@@ -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,
});
}
}}

View File

@@ -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({