working on manage org modal
This commit is contained in:
17
package-lock.json
generated
17
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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!);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"moduleResolution": "NodeNext", // or "node16"/"nodenext"
|
||||
"module": "NodeNext", // or "node16"/"nodenext"
|
||||
|
||||
"declaration": true,
|
||||
// "declaration": true,
|
||||
"rootDir": ".",
|
||||
"baseUrl": ".",
|
||||
"outDir": "./dist",
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
17
shared/drizzle/0008_fresh_jackpot.sql
Normal file
17
shared/drizzle/0008_fresh_jackpot.sql
Normal 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;
|
||||
3106
shared/drizzle/meta/0008_snapshot.json
Normal file
3106
shared/drizzle/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
15
shared/models/orgModels/frontendOrg.ts
Normal file
15
shared/models/orgModels/frontendOrg.ts
Normal 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>;
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
28
vite/src/hooks/useOrg.tsx
Normal 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 };
|
||||
};
|
||||
46
vite/src/hooks/useOrgId.tsx
Normal file
46
vite/src/hooks/useOrgId.tsx
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()],
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, "");
|
||||
};
|
||||
|
||||
|
||||
@@ -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>;
|
||||
// }
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
104
vite/src/views/main-sidebar/components/CreateNewOrg.tsx
Normal file
104
vite/src/views/main-sidebar/components/CreateNewOrg.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
29
vite/src/views/main-sidebar/components/LogOutItem.tsx
Normal file
29
vite/src/views/main-sidebar/components/LogOutItem.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
158
vite/src/views/main-sidebar/components/OrgDropdown.tsx
Normal file
158
vite/src/views/main-sidebar/components/OrgDropdown.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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">
|
||||
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">
|
||||
// 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>
|
||||
// );
|
||||
// };
|
||||
|
||||
Reference in New Issue
Block a user