working on manage org modal

This commit is contained in:
John Yeo
2025-06-13 10:58:53 +01:00
parent 50e8a536b7
commit f9cd9c4078
51 changed files with 3957 additions and 365 deletions

17
package-lock.json generated
View File

@@ -11057,7 +11057,6 @@
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"ai": "^4.3.10",
"autumn-js": "^0.0.46",
"better-auth": "^1.2.9",
"body-parser": "^1.20.3",
"bullmq": "^5.31.1",
@@ -11109,22 +11108,6 @@
"typescript": "^5.7.3"
}
},
"server/node_modules/autumn-js": {
"version": "0.0.46",
"license": "MIT",
"dependencies": {
"rou3": "^0.6.1"
},
"peerDependencies": {
"@tanstack/react-query": "^5.76.1",
"react": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
},
"server/node_modules/date-fns": {
"version": "4.1.0",
"license": "MIT",

View File

@@ -45,7 +45,6 @@
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"ai": "^4.3.10",
"autumn-js": "^0.0.46",
"better-auth": "^1.2.9",
"body-parser": "^1.20.3",
"bullmq": "^5.31.1",

View File

@@ -3,12 +3,11 @@ dotenv.config();
import postgres from "postgres";
import { drizzle } from "drizzle-orm/postgres-js";
import { authSchema, schemas as schema } from "@autumn/shared";
import { schemas as schema } from "@autumn/shared";
export let client = postgres(process.env.DATABASE_URL!);
export let db = drizzle(client, { schema });
export let authDb = drizzle(client, { schema: authSchema });
export const initDrizzle = () => {
if (!client) {

View File

@@ -27,12 +27,6 @@ apiRouter.use(apiAuthMiddleware);
apiRouter.use(pricingMiddleware);
apiRouter.use(analyticsMiddleware);
apiRouter.get("/auth", (req: any, res) => {
res.json({
message: `Authenticated -- Hello ${req.minOrg?.slug}!`,
});
});
apiRouter.use("/customers", cusRouter);
apiRouter.use("/invoices", invoiceRouter);
apiRouter.use("/products", productApiRouter);

View File

@@ -257,10 +257,6 @@ const handleAttachOld = async (req: any, res: any) =>
}
logger.info("--------------------------------");
let publicStr = req.isPublic ? "(Public) " : "";
logger.info(
`${publicStr}ATTACH PRODUCT REQUEST (from ${req.minOrg.slug})`,
);
const {
customer,

View File

@@ -40,7 +40,7 @@ export const handleGetCustomer = async (req: any, res: any) =>
if (!customer) {
req.logtail.warn(
`GET /customers/${customerId}: not found | Org: ${req.minOrg.slug}`,
`GET /customers/${customerId}: not found | Org: ${org.slug}`,
);
res.status(StatusCodes.NOT_FOUND).json({
message: `Customer ${customerId} not found`,

View File

@@ -56,9 +56,7 @@ export const handlePostCustomerRequest = async (req: any, res: any) => {
error instanceof RecaseError &&
error.code === ErrCode.DuplicateCustomerId
) {
logger.warn(
`POST /customers: ${error.message} (org: ${req.minOrg.slug})`,
);
logger.warn(`POST /customers: ${error.message} (org: ${req.org?.slug})`);
res.status(error.statusCode).json({
message: error.message,
code: error.code,

View File

@@ -82,7 +82,7 @@ export const handleUpdateBalances = async (req: any, res: any) => {
logger.info("--------------------------------");
logger.info(
`REQUEST: UPDATE BALANCES FOR CUSTOMER ${customer.id}, ORG: ${req.minOrg.slug}`,
`REQUEST: UPDATE BALANCES FOR CUSTOMER ${customer.id}, ORG: ${org.slug}`,
);
logger.info(
`Features to update: ${balances.map(

View File

@@ -57,9 +57,7 @@ devRouter.post("/api_key", withOrgAuth, async (req: any, res) =>
name,
orgId,
prefix,
meta: {
org_slug: req.minOrg.slug,
},
meta: {},
});
res.status(200).json({

View File

@@ -53,11 +53,12 @@ mainRouter.use(
autumnHandler({
autumn: (req: any) => {
let bearerToken = parseAuthHeader(req);
// let bearerToken = parseAuthHeader(req);
return new Autumn({
secretKey: bearerToken,
// secretKey: bearerToken,
url: "http://localhost:8080/v1",
headers: req.headers,
}) as any;
},
identify: async (req: any) => {

View File

@@ -15,10 +15,9 @@ export const handlePostOrg = async (req: any, res: any) =>
console.log("userId", userId);
const userMemberships = await auth.api.({
userId: userId!,
});
// const userMemberships = await auth.api.({
// userId: userId!,
// });
// const clerk = createClerkCli();
// const user = await clerk.users.getUser(userId!);

View File

@@ -16,6 +16,7 @@ import { createStripeCli } from "@/external/stripe/utils.js";
import { AppEnv } from "@autumn/shared";
import { nullish } from "@/utils/genUtils.js";
import { clearOrgCache } from "./orgUtils/clearOrgCache.js";
import { createOrgResponse } from "./orgUtils.js";
export const orgRouter = express.Router();
@@ -30,9 +31,7 @@ orgRouter.get("", async (req: any, res) => {
const org = await OrgService.getFromReq(req);
res.status(200).json({
org,
});
res.status(200).json(createOrgResponse(org));
} catch (error) {
handleRequestError({
req,

View File

@@ -1,6 +1,6 @@
import { decryptData, generatePublishableKey } from "@/utils/encryptUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { AppEnv, ErrCode, Organization } from "@autumn/shared";
import { AppEnv, ErrCode, FrontendOrg, Organization } from "@autumn/shared";
import { createSvixApp } from "@/external/svix/svixUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { OrgService } from "./OrgService.js";
@@ -104,13 +104,15 @@ export const initDefaultConfig = () => {
};
};
export const createOrgResponse = (org: Organization) => {
export const createOrgResponse = (org: Organization): FrontendOrg => {
return {
id: org.id,
name: org.name,
logo: org.logo,
slug: org.slug,
default_currency: org.default_currency,
stripe_connected: org.stripe_connected,
created_at: org.created_at,
default_currency: org.default_currency || "usd",
stripe_connected: org.stripe_connected || false,
created_at: new Date(org.createdAt).getTime(),
test_pkey: org.test_pkey,
live_pkey: org.live_pkey,
};

View File

@@ -1,6 +1,6 @@
import { OrgService } from "@/internal/orgs/OrgService.js";
import { auth } from "@/utils/auth.js";
import { AuthType } from "@autumn/shared";
import { AuthType, ErrCode } from "@autumn/shared";
import { verifyToken } from "@clerk/express";
import { fromNodeHeaders } from "better-auth/node";
import { NextFunction } from "express";
@@ -35,58 +35,57 @@ const getTokenData = async (req: any, res: any) => {
};
export const withOrgAuth = async (req: any, res: any, next: NextFunction) => {
try {
const { logtail: logger } = req;
try {
// let tokenData = await getTokenData(req, res);
const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});
if (!session) {
logger.info(`Unauthorized - no session found`);
logger.info(`Unauthorized - no session found (${req.originalUrl})`);
return res
.status(401)
.json({ message: "Unauthorized - no session found" });
}
throw new Error("test");
const orgId = session?.session?.activeOrganizationId;
// if (!tokenData?.org_id) {
// throw new Error("token data has no org_id");
// }
if (!orgId) {
logger.info(`Unauthorized - no org id found`);
return res
.status(401)
.json({ message: "Unauthorized - no org id found" });
}
// let tokenOrg = tokenData!.org as any;
// let data = await OrgService.getWithFeatures({
// db: req.db,
// orgId: tokenOrg.id,
// env: req.env,
// });
let data = await OrgService.getWithFeatures({
db: req.db,
orgId: orgId,
env: req.env,
});
// if (!data) {
// return res.status(404).json({ message: "Org not found" });
// }
if (!data) {
logger.warn(`Org ${orgId} not found in DB`);
return res
.status(500)
.json({ message: "Org not found", code: ErrCode.OrgNotFound });
}
// const { org, features } = data;
const { org, features } = data;
// req.minOrg = {
// id: tokenOrg?.id,
// slug: tokenOrg?.slug,
// };
// req.orgId = tokenData!.org_id;
// req.user = tokenData!.user;
// req.org = org;
// req.features = features;
// req.authType = AuthType.Dashboard;
req.user = session?.user;
req.orgId = orgId;
req.org = org;
req.features = features;
req.authType = AuthType.Dashboard;
next();
} catch (error: any) {
console.log(
// `withOrgAuth error (${req.headers["authorization"]}):`,
`(warning) clerk auth failed:`,
error?.message || error,
);
// console.log(`(warning) clerk auth failed:`, error?.message || error);
logger.warn(`(warning) withOrgAuth failed:`, error?.message || error);
res.status(401).json({ message: "Unauthorized" });
return;
}

View File

@@ -2,13 +2,64 @@ import { betterAuth } from "better-auth";
import { emailOTP, admin, organization } from "better-auth/plugins";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { authDb } from "@/db/initDrizzle.js"; // your drizzle instance
// import { authDb } from "@/db/initDrizzle.js"; // your drizzle instance
import { saveOrgToDB } from "@/external/webhooks/clerkWebhooks.js";
import { db } from "@/db/initDrizzle.js";
import { member, session as sessionTable } from "@autumn/shared";
import { desc, eq } from "drizzle-orm";
export const auth = betterAuth({
database: drizzleAdapter(authDb, {
database: drizzleAdapter(db, {
provider: "pg", // or "mysql", "sqlite"
}),
databaseHooks: {
session: {
create: {
before: async (session) => {
let lastSession = await db
.select()
.from(sessionTable)
.where(eq(sessionTable.userId, session.userId))
.orderBy(desc(sessionTable.createdAt))
.limit(1);
if (!lastSession) {
return {
data: {
...session,
activeOrganizationId: null,
},
};
}
if (lastSession[0].activeOrganizationId) {
return {
data: {
...session,
activeOrganizationId: lastSession[0].activeOrganizationId,
},
};
}
let memberships = await db
.select()
.from(member)
.where(eq(member.userId, session.userId));
if (memberships.length > 0) {
return {
data: {
...session,
activeOrganizationId: memberships[0].organizationId,
},
};
}
return { data: session };
},
},
},
},
user: {
deleteUser: {
enabled: true,
@@ -41,11 +92,11 @@ export const auth = betterAuth({
organizationCreation: {
disabled: false,
afterCreate: async ({ organization, user }) => {
// await saveOrgToDB({
// db,
// id: org.id,
// slug: org.slug,
// });
await saveOrgToDB({
db,
id: organization.id,
slug: organization.slug,
});
},
},
}),

View File

@@ -113,9 +113,7 @@ export const handleRequestError = ({
// logger.warn(`${req.method} ${req.originalUrl}`);
logReqUrl(logger, req, "warn");
logger.warn(
`Request from ${
req.minOrg?.slug || req.org?.slug || req.orgId || "unknown"
} for ${action}`,
`Request from ${req.org?.slug || req.orgId || "unknown"} for ${action}`,
);
error.print(logger);
if (req.originalUrl.includes("/webhooks/stripe")) {
@@ -139,9 +137,7 @@ export const handleRequestError = ({
// logger.error(`${req.method} ${req.originalUrl}`);
logReqUrl(logger, req, "error");
logger.error(
`Request from ${
req.minOrg?.slug || req.org?.slug || req.orgId || "unknown"
} for ${action}`,
`Request from ${req.org?.slug || req.orgId || "unknown"} for ${action}`,
);
if (error instanceof Stripe.errors.StripeError) {

View File

@@ -1,10 +1,4 @@
import {
AppEnv,
AuthType,
Feature,
MinOrg,
Organization,
} from "@autumn/shared";
import { AppEnv, AuthType, Feature, Organization } from "@autumn/shared";
import { Logtail } from "@logtail/node";
import type {
Request as ExpressRequest,
@@ -27,7 +21,6 @@ export interface ExtendedRequest extends ExpressRequest {
orgId: string;
env: AppEnv;
minOrg: MinOrg;
org: Organization;
features: Feature[];

View File

@@ -24,7 +24,9 @@ export const routeHandler = async ({
try {
if (error instanceof RecaseError) {
if (error.code === ErrCode.EntityNotFound) {
req.logtail.warn(`${error.message}, org: ${req.minOrg?.slug}`);
req.logtail.warn(
`${error.message}, org: ${req.org?.slug || req.orgId}`,
);
return res.status(404).json({
message: error.message,
code: error.code,

View File

@@ -8,7 +8,7 @@
"moduleResolution": "NodeNext", // or "node16"/"nodenext"
"module": "NodeNext", // or "node16"/"nodenext"
"declaration": true,
// "declaration": true,
"rootDir": ".",
"baseUrl": ".",
"outDir": "./dist",

View File

@@ -3,11 +3,6 @@
import { z } from "zod";
import { OrgConfigSchema } from "../models/orgModels/orgConfig.js";
export const MinOrgSchema = z.object({
id: z.string(),
slug: z.string(),
});
export const StripeConfigSchema = z.object({
test_api_key: z.string(),
live_api_key: z.string(),
@@ -41,7 +36,6 @@ export const OrganizationSchema = z.object({
api_version: z.number().nullish(),
});
export type MinOrg = z.infer<typeof MinOrgSchema>;
export type Organization = z.infer<typeof OrganizationSchema>;
export type StripeConfig = z.infer<typeof StripeConfigSchema>;
export type SvixConfig = z.infer<typeof SvixConfigSchema>;

View File

@@ -40,7 +40,7 @@ export const session = pgTable("session", {
.references(() => user.id, { onDelete: "cascade" }),
impersonatedBy: text("impersonated_by"),
activeOrganizationId: text("active_organization_id"),
});
}).enableRLS();
export const account = pgTable("account", {
id: text("id").primaryKey(),
@@ -58,7 +58,7 @@ export const account = pgTable("account", {
password: text("password"),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
});
}).enableRLS();
export const verification = pgTable("verification", {
id: text("id").primaryKey(),
@@ -71,7 +71,7 @@ export const verification = pgTable("verification", {
updatedAt: timestamp("updated_at").$defaultFn(
() => /* @__PURE__ */ new Date(),
),
});
}).enableRLS();
// export const organization = pgTable("organization", {
// id: text("id").primaryKey(),
@@ -92,7 +92,7 @@ export const member = pgTable("member", {
.references(() => user.id, { onDelete: "cascade" }),
role: text("role").default("member").notNull(),
createdAt: timestamp("created_at").notNull(),
});
}).enableRLS();
export const invitation = pgTable("invitation", {
id: text("id").primaryKey(),
@@ -106,7 +106,7 @@ export const invitation = pgTable("invitation", {
inviterId: text("inviter_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
});
}).enableRLS();
export const authSchema = {
user,

View File

@@ -68,7 +68,14 @@ import { actions } from "../models/analyticsModels/actionTable.js";
import { events } from "../models/eventModels/eventTable.js";
import { replaceables } from "../models/cusProductModels/cusEntModels/replaceableTable.js";
import { user, session, account, verification, member } from "./auth-schema.js";
import {
user,
session,
account,
verification,
member,
invitation,
} from "./auth-schema.js";
export {
// Tables
@@ -98,6 +105,14 @@ export {
events,
replaceables,
// Auth
user,
session,
account,
verification,
member,
invitation,
// Relations
organizationsRelations,
entitlementsRelations,

View File

@@ -0,0 +1,17 @@
CREATE TABLE "invitation" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"email" text NOT NULL,
"role" text,
"status" text DEFAULT 'pending' NOT NULL,
"expires_at" timestamp NOT NULL,
"inviter_id" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "invitation" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
ALTER TABLE "account" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
ALTER TABLE "member" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
ALTER TABLE "session" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
ALTER TABLE "verification" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;

File diff suppressed because it is too large Load Diff

View File

@@ -43,6 +43,27 @@
"when": 1749746944226,
"tag": "0005_young_wendell_rand",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1749752804406,
"tag": "0006_bizarre_maestro",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1749752870654,
"tag": "0007_slim_talisman",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1749752911832,
"tag": "0008_fresh_jackpot",
"breakpoints": true
}
]
}

View File

@@ -10,6 +10,7 @@ export * from "./models/genModels/genModels.js";
// 1. Org Models
export * from "./models/orgModels/orgTable.js";
export * from "./models/orgModels/orgConfig.js";
export * from "./models/orgModels/frontendOrg.js";
// 2. Feature Models
export * from "./models/featureModels/featureTable.js";

View File

@@ -0,0 +1,15 @@
import { z } from "zod";
export const FrontendOrgSchema = z.object({
id: z.string(),
name: z.string(),
logo: z.string().nullable(),
slug: z.string(),
default_currency: z.string(),
stripe_connected: z.boolean(),
created_at: z.number(),
test_pkey: z.string().nullable(),
live_pkey: z.string().nullable(),
});
export type FrontendOrg = z.infer<typeof FrontendOrgSchema>;

View File

@@ -11,11 +11,6 @@ import {
import { OrgConfig } from "./orgConfig.js";
import { sql } from "drizzle-orm";
export type MinOrg = {
id: string;
slug: string;
};
export type SvixConfig = {
sandbox_app_id: string;
live_app_id: string;

View File

@@ -5,8 +5,6 @@ import { getRedirectUrl, navigateTo } from "@/utils/genUtils";
import LoadingScreen from "@/views/general/LoadingScreen";
import { MainSidebar } from "@/views/main-sidebar/MainSidebar";
import { AppEnv } from "@autumn/shared";
import { RedirectToSignIn, useUser } from "@clerk/clerk-react";
import { useOrganization } from "@clerk/clerk-react";
import { useEffect } from "react";
import { Navigate, Outlet, useLocation, useNavigate } from "react-router";
@@ -19,15 +17,14 @@ import { useSession } from "@/lib/auth-client";
export function MainLayout() {
const env = useEnv();
// const { isLoaded: isUserLoaded, user } = useUser();
// const { organization: org } = useOrganization();
const { getToken } = useAuth();
const { pathname } = useLocation();
const { data, isPending } = useSession();
const navigate = useNavigate();
const posthog = usePostHog();
const { data, isPending } = useSession();
const orgId = data?.session.activeOrganizationId;
useEffect(() => {
// Identify user
@@ -81,7 +78,7 @@ export function MainLayout() {
return;
}
if (!pathname.includes("/onboarding")) {
if (!orgId && !pathname.includes("/onboarding")) {
return (
<Navigate
to={getRedirectUrl("/onboarding", AppEnv.Sandbox)}

View File

@@ -1,11 +1,10 @@
import { Check } from "lucide-react";
import { useAuth, useOrganization, useUser } from "@clerk/clerk-react";
import { Tooltip, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
import { TooltipContent } from "../ui/tooltip";
import { Copy } from "lucide-react";
import { useState } from "react";
import { notNullish } from "@/utils/genUtils";
import { useSession } from "@/lib/auth-client";
export const AdminHover = ({
children,
@@ -16,13 +15,17 @@ export const AdminHover = ({
texts: (string | { key: string; value: string } | undefined | null)[];
hide?: boolean;
}) => {
const { isLoaded, user } = useUser();
const { actor } = useAuth();
const { data, isPending } = useSession();
const email = user?.primaryEmailAddress?.emailAddress;
const user = data?.user;
// const { isLoaded, user } = useUser();
// const { actor } = useAuth();
const email = user?.email;
const isAdmin =
notNullish(actor) ||
// notNullish(actor) ||
email === "johnyeocx@gmail.com" ||
email === "ayush@recaseai.com" ||
email === "johnyeo10@gmail.com" ||
@@ -36,7 +39,7 @@ export const AdminHover = ({
<TooltipTrigger className="w-fit !cursor-default">
{children}
</TooltipTrigger>
{isLoaded && (
{!isPending && (
<TooltipContent
className="bg-white/50 backdrop-blur-sm shadow-sm border-1 px-2 pr-6 py-2"
align="start"

View File

@@ -113,7 +113,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
>
{isLoading && <LoaderCircle className="animate-spin" size={14} />}
{startIcon && !isLoading && <>{startIcon}</>}
{variant == "add" && !disableStartIcon && <PlusIcon size={12} />}
{!isLoading && variant == "add" && !disableStartIcon && (
<PlusIcon size={12} />
)}
{children}
{endIcon && !isLoading && <>{endIcon}</>}
</Comp>

View File

@@ -39,7 +39,7 @@ const DialogOverlay = React.forwardRef<
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 z-50 bg-white/70 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
className,
)}
{...props}
/>
@@ -62,7 +62,7 @@ function DialogContent({
bg-stone-50
min-w-sm
`,
className
className,
)}
{...props}
>
@@ -86,13 +86,19 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
function DialogFooter({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & { variant?: "new" | "default" }) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
variant == "new" &&
"bg-stone-100 flex items-center h-10 gap-0 border-t border-zinc-200",
className,
)}
{...props}
/>

View File

@@ -23,19 +23,20 @@ const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
withIcon?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
>(({ className, inset, children, withIcon = true, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-zinc-100 data-[state=open]:bg-zinc-100 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus:bg-zinc-800 dark:data-[state=open]:bg-zinc-800",
inset && "pl-8",
className
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
{withIcon && <ChevronRight className="ml-auto" />}
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
@@ -49,7 +50,7 @@ const DropdownMenuSubContent = React.forwardRef<
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-zinc-200 bg-white p-1 text-zinc-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
className
className,
)}
{...props}
/>
@@ -69,7 +70,7 @@ const DropdownMenuContent = React.forwardRef<
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-zinc-200 bg-white p-1 text-zinc-950 shadow-md dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
className,
)}
{...props}
/>
@@ -82,8 +83,9 @@ const DropdownMenuItem = React.forwardRef<
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
isLoading?: boolean;
shimmer?: boolean;
}
>(({ className, inset, isLoading, ...props }, ref) => (
>(({ className, inset, shimmer = false, isLoading, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
@@ -92,8 +94,9 @@ const DropdownMenuItem = React.forwardRef<
hover:bg-zinc-100
`,
inset && "pl-8",
className
className,
)}
disabled={shimmer || isLoading}
{...props}
>
{isLoading ? (
@@ -101,6 +104,8 @@ const DropdownMenuItem = React.forwardRef<
{props.children}
<SmallSpinner />
</>
) : shimmer ? (
<div className="shimmer rounded">{props.children}</div>
) : (
props.children
)}
@@ -116,7 +121,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
className
className,
)}
checked={checked}
{...props}
@@ -140,7 +145,7 @@ const DropdownMenuRadioItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
className
className,
)}
{...props}
>
@@ -165,7 +170,7 @@ const DropdownMenuLabel = React.forwardRef<
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
className,
)}
{...props}
/>

28
vite/src/hooks/useOrg.tsx Normal file
View File

@@ -0,0 +1,28 @@
import {
authClient,
useListOrganizations,
useSession,
} from "@/lib/auth-client";
import { useAxiosSWR } from "@/services/useAxiosSwr";
import { FrontendOrg } from "@autumn/shared";
import { useEffect } from "react";
export const useOrg = () => {
const { data, isLoading, mutate } = useAxiosSWR({
url: "/organization",
});
// const { data: orgList } = useListOrganizations();
// useEffect(() => {
// if (!data && orgList?.length === 1) {
// authClient.organization.setActive({
// organizationId: orgList[0].id,
// });
// mutate();
// }
// }, [data, orgList]);
return { org: data as FrontendOrg, isLoading, mutate };
};

View File

@@ -0,0 +1,46 @@
import {
authClient,
useListOrganizations,
useSession,
} from "@/lib/auth-client";
import { useAxiosSWR } from "@/services/useAxiosSwr";
import { FrontendOrg } from "@autumn/shared";
import { useEffect, useState } from "react";
export const useOrgId = () => {
const { data: session, isPending: sessionPending } = useSession();
const { data: organizations, isPending: orgsPending } =
useListOrganizations();
const [orgId, setOrgId] = useState<string | null>(null);
const setFirstOrg = async () => {
if (organizations?.length === 1) {
await authClient.organization.setActive({
organizationId: organizations[0].id,
});
}
};
useEffect(() => {
const activeId = session?.session.activeOrganizationId;
if (activeId) {
setOrgId(activeId);
}
}, [session]);
// useEffect(() => {
// if (!orgId && organizations?.length && organizations.length > 0) {
// setFirstOrg();
// }
// }, [session, organizations]);
useEffect(() => {
console.log("OrgId", orgId);
}, [orgId]);
return {
orgId,
isLoading: sessionPending,
};
};

View File

@@ -1,14 +0,0 @@
import { useOrganization, useSession } from "@clerk/clerk-react";
import { useEffect } from "react";
export const useSessionClaims = () => {
const { isLoaded, session } = useSession();
const { isLoaded: isOrgLoaded, organization } = useOrganization();
useEffect(() => {}, [isOrgLoaded, isLoaded]);
if (!isLoaded) {
return { isLoaded: false, claims: null };
}
return { isLoaded: true, claims: session?.lastActiveToken?.jwt?.claims };
};

View File

@@ -255,3 +255,29 @@ button:focus-visible {
button {
cursor: pointer;
}
.shimmer {
position: relative;
overflow: hidden;
/* background-color: #e5e7eb; */
}
.shimmer::after {
content: "";
position: absolute;
top: 0;
left: -150%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.6),
transparent
);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
100% {
left: 150%;
}
}

View File

@@ -2,8 +2,19 @@ import { createAuthClient } from "better-auth/react";
import { emailOTPClient } from "better-auth/client/plugins";
import { organizationClient } from "better-auth/client/plugins";
export const { useSession, signIn, signUp, signOut, deleteUser } =
createAuthClient({
export const authClient = createAuthClient({
baseURL: "http://localhost:8080",
plugins: [emailOTPClient(), organizationClient()],
});
export const {
useSession,
signIn,
signUp,
signOut,
deleteUser,
useListOrganizations,
} = createAuthClient({
baseURL: "http://localhost:8080",
plugins: [emailOTPClient(), organizationClient()],
});

View File

@@ -1,11 +1,10 @@
import "./index.css";
import App from "./App";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import { ClerkProvider } from "@clerk/clerk-react";
import App from "./App";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import { AutumnProvider } from "autumn-js/react";
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
if (!PUBLISHABLE_KEY) {

View File

@@ -11,12 +11,12 @@ export function useAxiosSWR({
enabled = true,
}: {
url: string;
env: AppEnv;
env?: AppEnv;
withAuth?: boolean;
options?: SWRConfiguration;
enabled?: boolean;
}) {
const axiosInstance = useAxiosInstance({ env, isAuth: withAuth });
const axiosInstance = useAxiosInstance({ isAuth: withAuth });
const fetcher = async (url: string) => {
const res = await axiosInstance.get(url);

View File

@@ -11,10 +11,13 @@ export const keyToTitleFirstCaps = (key: string) => {
return res.replace(/_/g, " ");
};
export const slugify = (text: string) => {
export const slugify = (
text: string,
type: "underscore" | "dash" = "underscore",
) => {
return text
.toLowerCase()
.replace(/ /g, "_")
.replace(/ /g, type == "underscore" ? "_" : "-")
.replace(/[^\w\s-]/g, "");
};

View File

@@ -1,5 +1,3 @@
import { RedirectToSignIn, useOrganization, useUser } from "@clerk/clerk-react";
import LoadingScreen from "./general/LoadingScreen";
import { Link, Navigate, useLocation } from "react-router";
import ErrorScreen from "./general/ErrorScreen";
@@ -18,11 +16,4 @@ export const DefaultView = () => {
</Link>
</ErrorScreen>
);
// By default, will come here
// 2. if user, redirect to customers
// return <Navigate to="/customers" replace={true} />;
// if (!user) {
// return <div>Hello World</div>;
// }
};

View File

@@ -8,6 +8,7 @@ import { signIn } from "@/lib/auth-client";
export const SignIn = () => {
const [email, setEmail] = useState("");
const [googleLoading, setGoogleLoading] = useState(false);
const handleEmailSignIn = (e: React.FormEvent) => {
e.preventDefault();
@@ -17,10 +18,19 @@ export const SignIn = () => {
};
const handleGoogleSignIn = async () => {
setGoogleLoading(true);
try {
await signIn.social({
provider: "google",
callbackURL: "http://localhost:3000/customers",
});
} catch (error) {
console.error("Error signing in with Google:", error);
} finally {
setTimeout(() => {
setGoogleLoading(false);
}, 1000);
}
};
return (
@@ -45,8 +55,12 @@ export const SignIn = () => {
variant="outline"
onClick={handleGoogleSignIn}
className="w-full h-10 font-medium text-sm gap-2"
>
// disabled={googleLoading}
isLoading={googleLoading}
startIcon={
<FontAwesomeIcon icon={faGoogle} className="text-zinc-400" />
}
>
Continue with Google
</Button>

View File

@@ -19,6 +19,7 @@ import { OrgService } from "@/services/OrgService";
import { CusProductStatus, Entity, Product } from "@autumn/shared";
import SmallSpinner from "@/components/general/SmallSpinner";
import { Search } from "lucide-react";
import { useOrg } from "@/hooks/useOrg";
function AddProduct() {
const { products, customer, env, entityId, entities } = useCustomerContext();
@@ -26,6 +27,7 @@ function AddProduct() {
const [options, setOptions] = useState<any[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [open, setOpen] = useState(false);
const { org } = useOrg();
const filteredProducts = products.filter((product: Product) => {
if (product.is_add_on && !searchQuery) return true;

View File

@@ -19,34 +19,36 @@ import {
} from "@/components/ui/tooltip";
import { Check, ChevronLeft, ChevronRight, Copy } from "lucide-react";
import { AdminHover } from "@/components/general/AdminHover";
import { OrgDropdown } from "./components/OrgDropdown";
export const SidebarTop = () => {
const { isLoaded, user } = useUser();
const { state, setState } = useSidebarContext();
const primaryEmail = user?.primaryEmailAddress?.emailAddress;
const env = useEnv();
const { organization } = useOrganization();
const prevOrgIdRef = useRef<string | null>(null);
// const { organization } = useOrganization();
// const prevOrgIdRef = useRef<string | null>(null);
useEffect(() => {
// Skip the first render
if (prevOrgIdRef.current === null) {
prevOrgIdRef.current = organization?.id || null;
return;
}
// useEffect(() => {
// // Skip the first render
// if (prevOrgIdRef.current === null) {
// prevOrgIdRef.current = organization?.id || null;
// return;
// }
// If organization changed (switched or created/deleted)
if (prevOrgIdRef.current !== (organization?.id || null)) {
console.log("Organization changed, refreshing page");
window.location.reload();
}
// // If organization changed (switched or created/deleted)
// if (prevOrgIdRef.current !== (organization?.id || null)) {
// console.log("Organization changed, refreshing page");
// window.location.reload();
// }
// Update the ref
prevOrgIdRef.current = organization?.id || null;
}, [organization]);
// // Update the ref
// prevOrgIdRef.current = organization?.id || null;
// }, [organization]);
return (
<div className="px-2">
<OrgDropdown />
<div
className={cn(
"flex items-center w-full",
@@ -56,7 +58,17 @@ export const SidebarTop = () => {
{state == "expanded" && (
<div className="flex flex-col">
<div className="flex relative w-full h-7">
{organization && (
<OrganizationSwitcher
appearance={{
elements: {
organizationSwitcherTrigger: "flex !pl- pr-1 max-w-[160px]",
},
}}
hidePersonal={true}
skipInvitationScreen={true}
afterCreateOrganizationUrl="/sandbox/onboarding"
/>
{/* {organization && (
<AdminHover
texts={[
{
@@ -69,19 +81,9 @@ export const SidebarTop = () => {
},
]}
>
<OrganizationSwitcher
appearance={{
elements: {
organizationSwitcherTrigger:
"flex !pl- pr-1 max-w-[160px]",
},
}}
hidePersonal={true}
skipInvitationScreen={true}
afterCreateOrganizationUrl="/sandbox/onboarding"
/>
</AdminHover>
)}
)} */}
</div>
</div>
)}

View File

@@ -0,0 +1,104 @@
import FieldLabel from "@/components/general/modal-components/FieldLabel";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { useOrg } from "@/hooks/useOrg";
import { authClient } from "@/lib/auth-client";
import { slugify } from "@/utils/formatUtils/formatTextUtils";
import { useState } from "react";
import { toast } from "sonner";
export const CreateNewOrg = ({
dialogType,
setDialogType,
}: {
dialogType: "create" | "manage" | null;
setDialogType: (dialogType: "create" | "manage" | null) => void;
}) => {
const { mutate } = useOrg();
const [name, setName] = useState("");
const [slugChanged, setSlugChanged] = useState(false);
const [slug, setSlug] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleCreate = async () => {
setIsLoading(true);
try {
const { data, error } = await authClient.organization.create({
name,
slug,
});
if (error) throw error;
await authClient.organization.setActive({
organizationId: data.id,
});
await mutate();
toast.success("Organization created");
setDialogType(null);
} catch (error: any) {
console.log(error);
toast.error(error.message);
} finally {
setIsLoading(false);
}
};
return (
<Dialog
open={!!dialogType}
onOpenChange={(open) => {
if (!open) setDialogType(null);
}}
>
<DialogTrigger asChild></DialogTrigger>
<DialogContent className="gap-0 p-0 rounded-xs min-w-[400px]">
<div className="p-6 flex flex-col gap-4">
<DialogHeader>
<DialogTitle>Create New Organization</DialogTitle>
</DialogHeader>
<div className="flex gap-4">
<div>
<FieldLabel>Name</FieldLabel>
<Input
value={name}
onChange={(e) => {
setName(e.target.value);
if (!slugChanged) {
setSlug(slugify(e.target.value));
}
}}
/>
</div>
<div>
<FieldLabel>Slug</FieldLabel>
<Input
value={slug}
onChange={(e) => {
if (!slugChanged) {
setSlug(slugify(e.target.value));
}
}}
/>
</div>
</div>
</div>
<DialogFooter variant="new">
<Button variant="add" onClick={handleCreate} isLoading={isLoading}>
Create
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,29 @@
import { authClient } from "@/lib/auth-client";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { LogOut } from "lucide-react";
import { useState } from "react";
export const LogOutItem = () => {
const [loading, setLoading] = useState(false);
return (
<DropdownMenuItem
onClick={async () => {
try {
setLoading(true);
await authClient.signOut();
window.location.reload();
} catch (error) {
console.error("Error signing out:", error);
} finally {
setLoading(false);
}
}}
>
<div className="flex justify-between w-full items-center gap-2 text-t2">
<span>Log Out</span>
<LogOut size={14} />
</div>
</DropdownMenuItem>
);
};

View File

@@ -0,0 +1,158 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useOrg } from "@/hooks/useOrg";
import {
authClient,
useListOrganizations,
useSession,
} from "@/lib/auth-client";
import { FrontendOrg } from "@autumn/shared";
import { DropdownMenuGroup } from "@radix-ui/react-dropdown-menu";
import { LogOut, Plus, Settings, Trash } from "lucide-react";
import React from "react";
import { useState } from "react";
import { CreateNewOrg } from "./CreateNewOrg";
import { toast } from "sonner";
import { LogOutItem } from "./LogOutItem";
import { cn } from "@/lib/utils";
const OrgLogo = ({ org }: { org: FrontendOrg }) => {
const firstLetter = org.name.charAt(0).toUpperCase();
return (
<div className="bg-primary/80 w-5 h-5 rounded-md flex items-center justify-center">
{org.logo ? (
<img src={org.logo} alt={org.name} className="w-full h-full" />
) : (
<span className="text-white text-xs">{firstLetter}</span>
)}
</div>
);
};
export const OrgDropdown = () => {
const { org, isLoading } = useOrg();
const { data: orgs, isPending } = useListOrganizations();
const [dialogType, setDialogType] = useState<"create" | "manage" | null>(
null,
);
const [dropdownOpen, setDropdownOpen] = useState(false);
if (!org) return null;
if (isLoading) return <div></div>;
return (
<React.Fragment>
<CreateNewOrg dialogType={dialogType} setDialogType={setDialogType} />
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button
className="p-2 h-7 gap-2 hover:bg-stone-200/60"
variant="ghost"
>
<OrgLogo org={org} />
<span className="text-t2">{org.name}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="border-1 border-zinc-200 shadow-sm w-48"
>
<DropdownMenuGroup>
<DropdownMenuItem>
<div className="flex justify-between w-full items-center gap-2 text-t2">
<span>Manage</span>
<Settings size={14} />
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
setDialogType("create");
}}
>
<div className="flex justify-between w-full items-center gap-2 text-t2">
<span>Create New</span>
<Plus size={14} />
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger className="text-t2">
Switch Organization
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="w-48">
{orgs?.map((org) => (
<SwitchOrgItem
key={org.id}
org={org}
setDropdownOpen={setDropdownOpen}
/>
))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<LogOutItem />
</DropdownMenuContent>
</DropdownMenu>
</React.Fragment>
);
};
const SwitchOrgItem = ({ org, setDropdownOpen }: any) => {
const [loading, setLoading] = useState(false);
const { mutate } = useOrg();
const handleSwitchOrg = async (orgId: string) => {
setLoading(true);
try {
await authClient.organization.setActive({
organizationId: orgId,
});
window.location.reload();
} catch (error: any) {
toast.error(error.message);
} finally {
setLoading(false);
}
};
return (
<DropdownMenuItem
key={org.id}
onClick={async (e) => {
e.preventDefault();
await handleSwitchOrg(org.id);
setDropdownOpen(false);
}}
shimmer={loading}
className="flex justify-between"
>
<span className={cn("text-t2")}>{org.name}</span>
</DropdownMenuItem>
);
};

View File

@@ -1,7 +1,6 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { useAuth, useOrganization } from "@clerk/clerk-react";
import { useSearchParams } from "react-router";
import Step from "@/components/general/OnboardingStep";
import { useAxiosSWR } from "@/services/useAxiosSwr";
@@ -22,21 +21,19 @@ import { SampleApp } from "./onboarding-steps/SampleApp";
import IntegrationGuideStep from "./onboarding-steps/IntegrationGuide";
import AutumnProviderStep from "./onboarding-steps/AutumnProvider";
import { AutumnProvider } from "autumn-js/react";
import { useSession } from "@/lib/auth-client";
function OnboardingView() {
const env = useEnv();
// const { organization: org } = useOrganization();
const [searchParams] = useSearchParams();
const token = searchParams.get("token");
const [apiKey, setApiKey] = useState("");
const [showIntegrationSteps, setShowIntegrationSteps] = useState(false);
const hasHandledToken = useRef(false);
const axiosInstance = useAxiosInstance();
const token = searchParams.get("token");
const [loading, setLoading] = useState(true);
const { getToken } = useAuth();
const { data } = useSession();
const orgId = data?.session?.activeOrganizationId;
const {
data: productData,
@@ -71,11 +68,11 @@ function OnboardingView() {
// }
// }, [org, searchParams, token, axiosInstance, productMutate]);
// useEffect(() => {
// if (org && !token) {
// setLoading(false);
// }
// }, [org, token]);
useEffect(() => {
if (orgId && !token) {
setLoading(false);
}
}, [orgId, token]);
if (loading || productLoading) {
return <LoadingScreen />;
@@ -94,13 +91,12 @@ function OnboardingView() {
/>
<AutumnProvider
backendUrl={`${import.meta.env.VITE_PUBLIC_BACKEND_URL}/demo`}
includeCredentials={false}
getBearerToken={async () => {
const token = await getToken({
template: "custom_template",
});
return token;
}}
// getBearerToken={async () => {
// const token = await getToken({
// template: "custom_template",
// });
// return token;
// }}
>
<SampleApp data={productData} mutate={productMutate} number={3} />
</AutumnProvider>

View File

@@ -1,6 +1,10 @@
import { useSession } from "@/lib/auth-client";
import {
authClient,
useListOrganizations,
useSession,
} from "@/lib/auth-client";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef } from "react";
export const useCreateOrg = ({
productMutate,
@@ -11,18 +15,28 @@ export const useCreateOrg = ({
const hasCreatedOrg = useRef(false);
const { data: session } = useSession();
// const { user } = useUser();
// const { organization: org } = useOrganization();
// const { setActive } = useOrganizationList();
const { data: organizations, isPending } = useListOrganizations();
useEffect(() => {
console.log("session", session);
const createDefaultOrg = async () => {
if (hasCreatedOrg.current) return;
if (hasCreatedOrg.current || isPending) return;
hasCreatedOrg.current = true;
// Either set first org active, or create a new org
try {
await axiosInstance.post("/organization");
if (organizations && organizations.length > 0) {
await authClient.organization.setActive({
organizationId: organizations[0]?.id,
});
} else {
await authClient.organization.create({
name: `${session?.user.name}'s Org`,
slug: crypto.randomUUID(),
});
await productMutate();
}
// await axiosInstance.post("/organization");
// await authClient.use
// if (
// user?.organizationMemberships?.length &&
// user.organizationMemberships.length > 0
@@ -30,7 +44,7 @@ export const useCreateOrg = ({
// await setActive?.({
// organization: user.organizationMemberships[0].organization.id,
// });
// await productMutate();
// return;
// } else {
// const { data } = await axiosInstance.post("/organization");

View File

@@ -3,125 +3,122 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { useEnv } from "@/utils/envUtils";
import { useOrganization, useOrganizationList } from "@clerk/clerk-react";
import { Building } from "lucide-react";
import { useState } from "react";
import ConfettiExplosion from "react-confetti-explosion";
import { toast } from "sonner";
export const CreateOrgStep = ({
number,
pollForOrg,
}: {
number: number;
pollForOrg: () => Promise<void>;
}) => {
const { organization: org } = useOrganization();
const env = useEnv();
const axios = useAxiosInstance({ env });
const { createOrganization, setActive } = useOrganizationList();
// export const CreateOrgStep = ({
// number,
// pollForOrg,
// }: {
// number: number;
// pollForOrg: () => Promise<void>;
// }) => {
const [isExploding, setIsExploding] = useState(false);
const [loading, setLoading] = useState(false);
const [fields, setFields] = useState({
name: org?.name || "",
slug: "",
});
// const env = useEnv();
// const axios = useAxiosInstance({ env });
const handleCreateOrg = async () => {
setLoading(true);
// const [isExploding, setIsExploding] = useState(false);
// const [loading, setLoading] = useState(false);
// const [fields, setFields] = useState({
// name: org?.name || "",
// slug: "",
// });
try {
if (!createOrganization) {
toast.error("Error creating organization");
return;
}
// const handleCreateOrg = async () => {
// setLoading(true);
const org = await createOrganization({
name: fields.name,
});
// try {
// if (!createOrganization) {
// toast.error("Error creating organization");
// return;
// }
// Create org in Autumn
const res = await axios.post("/organization", {
orgId: org.id,
});
// const org = await createOrganization({
// name: fields.name,
// });
console.log("Org created in Autumn", res);
// // Create org in Autumn
// const res = await axios.post("/organization", {
// orgId: org.id,
// });
await setActive({ organization: org.id });
// await pollForOrg();
toast.success(`Created your organization: ${org.name}`);
setIsExploding(true);
} catch (error: any) {
if (error.message) {
toast.error(error.message);
} else {
toast.error("Error creating organization");
}
}
setLoading(false);
};
// console.log("Org created in Autumn", res);
return (
<Step
title="Create your organization"
number={number}
description={
<>
<div className="flex relative w-fit">
<div className="flex bg-purple-100 shadow-sm shadow-purple-500/50 w-fit px-3 py-0.5 rounded-lg absolute w-full h-full z-0"></div>
<p className="flex items-center border border-primary w-fit px-3 py-0.5 rounded-lg z-10">
<span className="animate-bounce">👋</span>
<span className="font-bold text-primary">
&nbsp; Welcome to Autumn
</span>
</p>
</div>
<p>
Create an organization to get started and integrate pricing within 5
minutes.
</p>
</>
}
>
{/* <div className="flex gap-8 w-full justify-between flex-col lg:flex-row"> */}
<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) => {
const newFields = { ...fields, name: e.target.value };
setFields(newFields);
}}
/>
<Button
className="min-w-44 w-44 max-w-44"
disabled={!!org?.name}
onClick={handleCreateOrg}
isLoading={loading}
variant="gradientPrimary"
// startIcon={<Building size={12} />}
>
Create Organization
</Button>
// await setActive({ organization: org.id });
// // await pollForOrg();
// toast.success(`Created your organization: ${org.name}`);
// setIsExploding(true);
// } catch (error: any) {
// if (error.message) {
// toast.error(error.message);
// } else {
// toast.error("Error creating organization");
// }
// }
// setLoading(false);
// };
{isExploding && (
<ConfettiExplosion
className="absolute"
force={0.8}
duration={3000}
particleCount={250}
zIndex={1000}
width={1600}
onComplete={() => {
console.log("complete");
}}
/>
)}
</div>
{/* </div> */}
</Step>
);
};
// return (
// <Step
// title="Create your organization"
// number={number}
// description={
// <>
// <div className="flex relative w-fit">
// <div className="flex bg-purple-100 shadow-sm shadow-purple-500/50 w-fit px-3 py-0.5 rounded-lg absolute w-full h-full z-0"></div>
// <p className="flex items-center border border-primary w-fit px-3 py-0.5 rounded-lg z-10">
// <span className="animate-bounce">👋</span>
// <span className="font-bold text-primary">
// &nbsp; Welcome to Autumn
// </span>
// </p>
// </div>
// <p>
// Create an organization to get started and integrate pricing within 5
// minutes.
// </p>
// </>
// }
// >
// {/* <div className="flex gap-8 w-full justify-between flex-col lg:flex-row"> */}
// <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) => {
// const newFields = { ...fields, name: e.target.value };
// setFields(newFields);
// }}
// />
// <Button
// className="min-w-44 w-44 max-w-44"
// disabled={!!org?.name}
// onClick={handleCreateOrg}
// isLoading={loading}
// variant="gradientPrimary"
// // startIcon={<Building size={12} />}
// >
// Create Organization
// </Button>
// {isExploding && (
// <ConfettiExplosion
// className="absolute"
// force={0.8}
// duration={3000}
// particleCount={250}
// zIndex={1000}
// width={1600}
// onComplete={() => {
// console.log("complete");
// }}
// />
// )}
// </div>
// {/* </div> */}
// </Step>
// );
// };