quickstart guide

This commit is contained in:
Ayush
2025-02-11 14:28:09 +00:00
parent b1175f610e
commit 27a378cc6a
26 changed files with 1219 additions and 272 deletions

View File

@@ -36,7 +36,7 @@
"@supabase/ssr": "^0.5.2",
"@supabase/supabase-js": "^2.47.2",
"@useautumn/react": "latest",
"@wooorm/starry-night": "^3.5.0",
"@wooorm/starry-night": "^3.6.0",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -48,14 +48,17 @@
"lodash": "^4.17.21",
"lucide-react": "^0.468.0",
"next": "15.0.3",
"next-themes": "^0.4.4",
"ora": "^8.1.1",
"pointer-sdk": "^0.0.9",
"react": "^18.2.0",
"react-confetti-explosion": "^2.1.2",
"react-day-picker": "^8.10.1",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.6.1",
"react-use-websocket": "^4.13.0",
"sonner": "^1.7.4",
"swr": "^2.2.5",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",

View File

@@ -4,5 +4,21 @@ import ConnectStripe from "@/views/onboarding/ConnectStripe";
import { useSearchParams } from "next/navigation";
export default async function StripePage() {
return <ConnectStripe />;
return (
<>
<div className="flex flex-col items-center justify-center h-screen">
<div className="w-[430px] shadow-lg rounded-2xl border flex flex-col p-8 bg-white gap-4">
<div>
<p className="text-md font-bold text-t2">
Please connect your Stripe account
</p>
<p className="text-t3 text-xs mt-1">
Your credentials will be encrypted and stored safely
</p>
</div>
<ConnectStripe />
</div>
</div>
</>
);
}

View File

@@ -53,7 +53,7 @@ export default async function RootLayout({
<ClerkProvider>
<NextUIProvider>
<SidebarProvider>
{org_id && !path.includes("/demo") && (
{!path.includes("/demo") && (
<HomeSidebar
user={sessionClaims?.user as any}
org={org}
@@ -73,15 +73,22 @@ export default async function RootLayout({
</div>
)}
{path.includes("/onboarding") ? (
children
) : (
<div className="w-full h-full overflow-scroll bg-stone-50 p-6 flex justify-center">
<div className="w-full h-fit max-w-[1048px] flex flex-col gap-4">
{children}
<div className="w-full h-full overflow-scroll bg-stone-50 p-6 flex justify-center">
<div className="hidden md:flex w-full h-fit max-w-[1048px] flex-col gap-4">
{children}
</div>
<div className="md:hidden w-full h-full flex items-center justify-center">
<div className="bg-white p-6 rounded-lg shadow-sm text-center">
<h2 className="text-xl font-semibold mb-2">
Autumn is coming to mobile soon
</h2>
<p className="text-gray-600">
We're currently designed for larger screens. Come back
on your desktop?
</p>
</div>
</div>
)}
</div>
</main>
{/* </AutumnProvider> */}
</SidebarProvider>

View File

@@ -1,10 +1,16 @@
import OnboardingView from "@/views/onboarding/OnboardingView";
import { AppEnv } from "@autumn/shared";
import { headers } from "next/headers";
import { auth } from "@clerk/nextjs/server";
async function OnboardingPage() {
// const onboardingView = await OnboardingView();
const headersList = await headers();
const env = (headersList.get("env") as AppEnv) || AppEnv.Sandbox;
const { sessionClaims } = await auth();
return <OnboardingView sessionClaims={sessionClaims} />;
return <OnboardingView sessionClaims={sessionClaims} env={env} />;
}
export default OnboardingPage;

View File

@@ -0,0 +1,67 @@
import { common, createStarryNight } from "@wooorm/starry-night";
import { toHtml } from "hast-util-to-html";
import { Root } from "hast-util-to-html/lib/types";
import React from "react";
import "@wooorm/starry-night/style/dark";
import CopyButton from "@/components/general/CopyButton";
interface CodeDisplayProps {
code: string;
language: string;
}
const getStarryNight = async () => {
return await createStarryNight(common);
};
export const CodeDisplay: React.FC<CodeDisplayProps> = ({ code, language }) => {
const [highlightedCode, setHighlightedCode] = React.useState("");
// Format language name to look nicer
const formatLanguage = (lang: string) => {
return lang.charAt(0).toUpperCase() + lang.slice(1).toLowerCase();
};
React.useEffect(() => {
const highlight = async () => {
const starryNight = await getStarryNight();
const scope = starryNight.flagToScope(language);
if (scope) {
const tree = starryNight.highlight(code, scope);
const html = toHtml(tree as Root);
// Wrap the HTML in a div with inline styles
const styledHtml = `<div style="white-space: pre; display: inline-block; width: 10px;">${html}</div>`;
setHighlightedCode(styledHtml);
} else {
setHighlightedCode(
`<div style="white-space: pre; display: inline-block; width: 10px;">${code}</div>`
);
}
};
highlight();
}, [code, language]);
return (
<div className="text-sm overflow-x-auto bg-slate-900 rounded-sm px-2 pt-2 [&::-webkit-scrollbar]:bg-transparent [&::-webkit-scrollbar-track]:bg-transparent">
<div className="flex justify-between items-center w-full">
<div className="text-xs text-zinc-400 font-mono">
{formatLanguage(language)}
</div>
<CopyButton
text={code}
className="text-white hover:text-white/80 hover:bg-zinc-800"
/>
</div>
<pre className="overflow-x-auto px-4 py-1 [&::-webkit-scrollbar]:bg-transparent [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-zinc-600 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar]:h-2">
<div className="max-w-full">
<code
className={`language-${language} text-white`}
dangerouslySetInnerHTML={{ __html: highlightedCode }}
/>
</div>
</pre>
</div>
);
};

View File

@@ -3,9 +3,9 @@ import { Toaster } from "react-hot-toast";
export const CustomToaster = () => {
return (
<Toaster
position="bottom-center"
position="top-center"
toastOptions={{
duration: 2000,
duration: 4000,
style: { fontSize: "14px" },
}}
/>

View File

@@ -32,7 +32,7 @@ const buttonVariants = cva(
gradientPrimary:
"bg-gradient-to-b font-semibold border border-primary from-primary/65 to-primary text-white hover:from-primary hover:to-primary shadow-sm shadow-purple-500/50",
gradientSecondary:
"border border-stone-300 font-semibold bg-gradient-to-b from-white to-stone-100 text-t1 hover:from-stone-300 hover:to-stone-400 shadow-sm text-xs",
"border border-stone-300 font-semibold bg-gradient-to-b from-white to-stone-100 text-t1 hover:from-stone-300 hover:to-stone-400 shadow-sm",
},
size: {
default: "h-8 px-3 flex items-center gap-1",

View File

@@ -0,0 +1,31 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-white group-[.toaster]:text-zinc-950 group-[.toaster]:border-zinc-200 group-[.toaster]:shadow-lg dark:group-[.toaster]:bg-zinc-950 dark:group-[.toaster]:text-zinc-50 dark:group-[.toaster]:border-zinc-800",
description: "group-[.toast]:text-zinc-500 dark:group-[.toast]:text-zinc-400",
actionButton:
"group-[.toast]:bg-zinc-900 group-[.toast]:text-zinc-50 dark:group-[.toast]:bg-zinc-50 dark:group-[.toast]:text-zinc-900",
cancelButton:
"group-[.toast]:bg-zinc-100 group-[.toast]:text-zinc-500 dark:group-[.toast]:bg-zinc-800 dark:group-[.toast]:text-zinc-400",
},
}}
{...props}
/>
)
}
export { Toaster }

View File

@@ -0,0 +1,33 @@
import React from "react";
import { cn } from "@/lib/utils";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCoin } from "@fortawesome/pro-duotone-svg-icons";
interface StepProps {
title: string;
children: React.ReactNode;
className?: string;
description?: string;
}
function Step({ title, children, className, description }: StepProps) {
return (
<div
className={cn(
"relative pl-8 pb-8 border-l-2 border-purple-100 gap-4 flex flex-col",
className
)}
>
<div className="absolute -left-[17px] -top-1 flex items-center justify-center w-8 h-8 rounded-full bg-stone-50">
<FontAwesomeIcon icon={faCoin} className="text-primary w-5 h-5" />
</div>
<div className="flex flex-col gap-1">
<h1 className="text-t1 text-lg font-medium">{title}</h1>
{description && <p className="text-t3">{description}</p>}
</div>
{children}
</div>
);
}
export default Step;

View File

@@ -18,16 +18,22 @@ export default clerkMiddleware(async (auth, req) => {
const { sessionClaims }: { sessionClaims: any } = await auth();
if (!sessionClaims?.org_id && !req.nextUrl.pathname.includes("/onboarding")) {
console.log("Redirecting to onboarding");
console.log(req.nextUrl.pathname, "Redirecting to onboarding");
const onboardingUrl = new URL("/onboarding", req.url);
if (req.nextUrl.pathname !== "/") {
onboardingUrl.searchParams.set(
"toast",
"Please create an organization to continue"
);
}
return NextResponse.redirect(onboardingUrl);
}
if (sessionClaims?.org_id && req.nextUrl.pathname.includes("/onboarding")) {
const url = new URL("/", req.url);
console.log("Redirecting to home");
return NextResponse.redirect(url);
}
// if (sessionClaims?.org_id && req.nextUrl.pathname.includes("/onboarding")) {
// const url = new URL("/", req.url);
// console.log("Redirecting to home");
// return NextResponse.redirect(url);
// }
if (path === "/") {
return NextResponse.redirect(new URL("/customers", req.url));

View File

@@ -118,7 +118,7 @@ export const CustomerEntitlementsList = ({
<TableHead className="">
{featureType === FeatureType.Metered && "Balance"}
</TableHead>
<TableHead className="min-w-0 w-24">
<TableHead className="min-w-0 w-28">
{featureType === FeatureType.Metered && "Next Reset"}
</TableHead>
{/* <TableHead className="">Status</TableHead> */}

View File

@@ -17,7 +17,8 @@ import { useAxiosInstance } from "@/services/useAxiosInstance";
import { useDevContext } from "./DevContext";
const CreateAPIKey = () => {
const { env, mutate } = useDevContext();
const { env, mutate, onboarding, apiKeyName, setApiCreated, apiCreated } =
useDevContext();
const axiosInstance = useAxiosInstance({ env });
const [loading, setLoading] = useState(false);
@@ -39,13 +40,15 @@ const CreateAPIKey = () => {
}, [copied]);
const handleCreate = async () => {
console.log("creating api key", apiKeyName ? apiKeyName : name);
setLoading(true);
try {
const { api_key } = await DevService.createAPIKey(axiosInstance, {
name,
name: apiKeyName ? apiKeyName : name,
});
setApiKey(api_key);
setApiCreated && setApiCreated(true);
await mutate();
} catch (error) {
console.log("Error:", error);
@@ -58,7 +61,17 @@ const CreateAPIKey = () => {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="" startIcon={<Plus size={15} />} variant="dashed">
<Button
className={`${onboarding ? "w-fit" : "w-full"}`}
startIcon={<Plus size={15} />}
variant={onboarding ? "gradientPrimary" : "dashed"}
disabled={apiCreated ? true : false}
onClick={() => {
if (apiKeyName) {
handleCreate();
}
}}
>
Create API Key
</Button>
</DialogTrigger>

View File

@@ -0,0 +1,99 @@
import {
formatUnixToDateTime,
formatUnixToDateTimeString,
} from "@/utils/formatUtils/formatDateUtils";
import { Feature, FeatureType, Product } from "@autumn/shared";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { FeatureRowToolbar } from "./FeatureRowToolbar";
import {
Table,
TableHead,
TableHeader,
TableRow,
TableBody,
TableCell,
} from "@/components/ui/table";
import { navigateTo } from "@/utils/genUtils";
import { useFeaturesContext } from "./FeaturesContext";
import { Badge } from "@/components/ui/badge";
import UpdateFeature from "./UpdateFeature";
import { FeatureTypeBadge } from "./FeatureTypeBadge";
export const FeaturesTable = () => {
const { env, features, onboarding } = useFeaturesContext();
const router = useRouter();
const [open, setOpen] = useState(false);
const [selectedFeature, setSelectedFeature] = useState<any>(null);
const getMeteredEventNames = (feature: Feature) => {
if (feature.type !== FeatureType.Metered) return "";
if (!feature.config.filters || feature.config.filters.length === 0)
return "";
return feature.config.filters[0].value.join(", ");
};
const handleRowClick = (id: string) => {
const feature = features.find((feature: Feature) => feature.id === id);
setSelectedFeature(feature);
setOpen(true);
};
return (
<>
<UpdateFeature
open={open}
setOpen={setOpen}
selectedFeature={selectedFeature}
setSelectedFeature={setSelectedFeature}
/>
<Table>
<TableHeader className="rounded-full">
<TableRow className="border-none">
<TableHead className="">Name</TableHead>
<TableHead>ID</TableHead>
<TableHead>Type</TableHead>
{!onboarding && <TableHead>Event Names</TableHead>}
{!onboarding && (
<TableHead className="min-w-0 w-28">Created At</TableHead>
)}
<TableHead className="min-w-0 w-10"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{features.map((feature: Feature) => (
<TableRow
key={feature.internal_id}
className="cursor-pointer"
onClick={() => handleRowClick(feature.id)}
>
<TableCell>{feature.name}</TableCell>
<TableCell className="font-mono">{feature.id}</TableCell>
<TableCell>
<FeatureTypeBadge type={feature.type} />
</TableCell>
{!onboarding && (
<TableCell>{getMeteredEventNames(feature)}</TableCell>
)}
{!onboarding && (
<TableCell className="min-w-20 w-24">
<span>{formatUnixToDateTime(feature.created_at).date}</span>{" "}
<span className="text-t3">
{formatUnixToDateTime(feature.created_at).time}
</span>
</TableCell>
)}
<TableCell className="min-w-4 w-6">
<FeatureRowToolbar feature={feature} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
);
};

View File

@@ -7,6 +7,7 @@ import { Feature, FeatureType } from "@autumn/shared";
import { CreateFeature } from "./CreateFeature";
import { AppEnv } from "@autumn/shared";
import LoadingScreen from "../general/LoadingScreen";
import { FeaturesTable } from "./FeaturesTable";
import {
Table,
TableHeader,
@@ -25,8 +26,8 @@ import UpdateFeature from "./UpdateFeature";
import { CustomToaster } from "@/components/general/CustomToaster";
function FeaturesView({ env }: { env: AppEnv }) {
const [open, setOpen] = useState(false);
const [selectedFeature, setSelectedFeature] = useState<any>(null);
// const [open, setOpen] = useState(false);
// const [selectedFeature, setSelectedFeature] = useState<any>(null);
const { data, isLoading, error, mutate } = useAxiosSWR({
url: `/features`,
@@ -38,26 +39,26 @@ function FeaturesView({ env }: { env: AppEnv }) {
return <LoadingScreen />;
}
const handleRowClick = (id: string) => {
const feature = data?.features.find(
(feature: Feature) => feature.id === id
);
setSelectedFeature(feature);
setOpen(true);
};
// const handleRowClick = (id: string) => {
// const feature = data?.features.find(
// (feature: Feature) => feature.id === id
// );
// setSelectedFeature(feature);
// setOpen(true);
// };
const features = data?.features.filter(
(feature: Feature) => feature.type !== "credit_system"
);
const getMeteredEventNames = (feature: Feature) => {
if (feature.type !== FeatureType.Metered) return "";
// const getMeteredEventNames = (feature: Feature) => {
// if (feature.type !== FeatureType.Metered) return "";
if (!feature.config.filters || feature.config.filters.length === 0)
return "";
// if (!feature.config.filters || feature.config.filters.length === 0)
// return "";
return feature.config.filters[0].value.join(", ");
};
// return feature.config.filters[0].value.join(", ");
// };
return (
<FeaturesContext.Provider
@@ -75,8 +76,9 @@ function FeaturesView({ env }: { env: AppEnv }) {
Define the metered and boolean features your users are entitled to
</p>
</div>
<FeaturesTable />
<UpdateFeature
{/* <UpdateFeature
open={open}
setOpen={setOpen}
selectedFeature={selectedFeature}
@@ -128,7 +130,7 @@ function FeaturesView({ env }: { env: AppEnv }) {
</TableRow>
))}
</TableBody>
</Table>
</Table> */}
<CreateFeature />
</FeaturesContext.Provider>

View File

@@ -30,7 +30,7 @@ function LoadingScreen() {
}, []);
return (
<div className="flex h-screen w-full items-center justify-center flex-col gap-4">
<div className="flex h-screen overflow-hidden w-full items-center justify-center flex-col gap-4">
<LoaderCircle className="animate-spin text-primary" size={30} />
<p className="text-t2 font-mono text-xs font-medium">{loadingText}</p>
</div>

View File

@@ -34,8 +34,17 @@ import { cn } from "@/lib/utils";
import { useSession } from "@clerk/nextjs";
import { useAxiosSWR } from "@/services/useAxiosSwr";
import LoadingScreen from "../general/LoadingScreen";
import SmallSpinner from "@/components/general/SmallSpinner";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCcStripe, faStripeS } from "@fortawesome/free-brands-svg-icons";
function ConnectStripe() {
function ConnectStripe({
className,
onboarding,
}: {
className?: string;
onboarding?: boolean;
}) {
const router = useRouter();
const searchParams = useSearchParams();
const redirect = searchParams.get("redirect");
@@ -43,8 +52,8 @@ function ConnectStripe() {
const [testApiKey, setTestApiKey] = useState("");
const [liveApiKey, setLiveApiKey] = useState("");
const [successUrl, setSuccessUrl] = useState("");
const [defaultCurrency, setDefaultCurrency] = useState("");
const [successUrl, setSuccessUrl] = useState("https://useautumn.com");
const [defaultCurrency, setDefaultCurrency] = useState("USD");
const [isLoading, setIsLoading] = useState(false);
const {
@@ -59,7 +68,7 @@ function ConnectStripe() {
const org = orgData?.org;
const handleConnectStripe = async () => {
if (!testApiKey || !liveApiKey || !successUrl || !defaultCurrency) {
if (!testApiKey || !successUrl || !defaultCurrency) {
toast.error("Please fill in all fields");
return;
}
@@ -74,17 +83,15 @@ function ConnectStripe() {
try {
await OrgService.connectStripe(axiosInstance, {
testApiKey,
liveApiKey,
liveApiKey: onboarding ? testApiKey : liveApiKey,
successUrl,
defaultCurrency,
});
toast.success("Successfully connected to Stripe");
if (redirect) {
await mutateOrg();
if (redirect && !onboarding) {
navigateTo(redirect, router, AppEnv.Live);
} else {
router.push("/");
}
} catch (error) {
console.log("Failed to connect Stripe", error);
@@ -108,19 +115,23 @@ function ConnectStripe() {
};
if (isOrgLoading) {
return <LoadingScreen />;
return <SmallSpinner />;
}
if (org?.stripe_connected) {
return (
<div className="flex flex-col items-center justify-center h-screen">
<p className="text-md font-medium text-t3">
Stripe already connected 🎉🎉🎉
</p>
<div
className={cn(
"flex flex-col gap-4",
className,
onboarding && "flex-row justify-between items-center"
)}
>
<p className="text-t3 text-sm">Stripe Connected &nbsp; </p>
<Button
onClick={handleDisconnectStripe}
variant="gradientSecondary"
className="mt-4"
className={`${onboarding ? "w-fit" : ""}`}
isLoading={isDisconnecting}
>
Disconnect Stripe
@@ -130,80 +141,59 @@ function ConnectStripe() {
}
return (
<>
<div className={cn("flex flex-col font-regular gap-4", className)}>
<CustomToaster />
<div className="flex flex-col items-center justify-center h-screen">
<div className="w-[430px] shadow-lg rounded-2xl border flex flex-col p-8 bg-white">
<p className="text-md font-bold text-t2">
Please connect your Stripe account
</p>
<p className="text-t3 text-xs mt-1">
Your credentials will be encrypted and stored safely
</p>
<div className="flex flex-col font-regular mt-4 gap-4">
<div>
<FieldLabel>Test API Key</FieldLabel>
<Input
value={testApiKey}
onChange={(e) => setTestApiKey(e.target.value)}
/>
</div>
<div className="flex flex-col font-regular gap-4">
<div>
<FieldLabel>Stripe Test Secret API Key</FieldLabel>
<Input
value={testApiKey}
placeholder="sk_test_..."
onChange={(e) => setTestApiKey(e.target.value)}
/>
</div>
<div>
<FieldLabel>Live API Key</FieldLabel>
<Input
value={liveApiKey}
onChange={(e) => setLiveApiKey(e.target.value)}
/>
</div>
<div>
<FieldLabel>Success URL</FieldLabel>
<Input
value={successUrl}
onChange={(e) => setSuccessUrl(e.target.value)}
/>
</div>
<div>
<FieldLabel>Default Currency</FieldLabel>
<CurrencySelect
defaultCurrency={defaultCurrency}
setDefaultCurrency={setDefaultCurrency}
/>
{/* <Select
value={defaultCurrency}
onValueChange={(value) => setDefaultCurrency(value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{stripeCurrencyCodes.map((currency) => (
<SelectItem
key={currency.code}
value={currency.code}
onClick={() => setDefaultCurrency(currency.code)}
>
{currency.currency} - {currency.code}
</SelectItem>
))}
</SelectContent>
</Select> */}
</div>
<div className="flex justify-end mt-4">
<Button
className="w-fit"
onClick={handleConnectStripe}
isLoading={isLoading}
>
Connect Stripe
</Button>
</div>
{!onboarding && (
<div>
<FieldLabel>Stripe Live Secret API Key</FieldLabel>
<Input
value={liveApiKey}
placeholder="sk_live_..."
onChange={(e) => setLiveApiKey(e.target.value)}
/>
</div>
)}
<div className="flex gap-2 w-full">
<div className="w-full truncate">
<FieldLabel>Success URL after Stripe payment</FieldLabel>
<Input
value={successUrl}
onChange={(e) => setSuccessUrl(e.target.value)}
/>
</div>
<div className="w-1/4 min-w-32">
<FieldLabel>Currency</FieldLabel>
<CurrencySelect
defaultCurrency={defaultCurrency}
setDefaultCurrency={setDefaultCurrency}
/>
</div>
</div>
<div className="flex justify-end">
<Button
className="w-fit"
variant="gradientPrimary"
onClick={handleConnectStripe}
disabled={org?.stripe_connected}
isLoading={isLoading}
startIcon={<FontAwesomeIcon icon={faStripeS} className="mr-2" />}
>
Connect Stripe
</Button>
</div>
</div>
</>
</div>
);
}

View File

@@ -0,0 +1 @@
export default function ConnectStripeOAuth() {}

View File

@@ -1,38 +1,131 @@
"use client";
import React, { useState } from "react";
import FieldLabel from "@/components/general/modal-components/FieldLabel";
import React, { useState, useEffect } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { slugify } from "@/utils/formatUtils/formatTextUtils";
import { CustomToaster } from "@/components/general/CustomToaster";
import { toast } from "react-hot-toast";
import {
OrganizationList,
OrganizationSwitcher,
useOrganizationList,
} from "@clerk/nextjs";
import { useOrganizationList } from "@clerk/nextjs";
import { useSearchParams } from "next/navigation";
import Step from "@/components/ui/step";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import ConnectStripe from "./ConnectStripe";
import { FeaturesTable } from "../features/FeaturesTable";
import { AppEnv, Feature } from "@autumn/shared";
import { useAxiosSWR } from "@/services/useAxiosSwr";
import { FeaturesContext } from "../features/FeaturesContext";
import SmallSpinner from "@/components/general/SmallSpinner";
import ConfettiExplosion from "react-confetti-explosion";
import { createClient } from "@supabase/supabase-js";
import { navigateTo } from "@/utils/genUtils";
import { useRouter } from "next/navigation";
import LoadingScreen from "../general/LoadingScreen";
import { CreateFeature } from "../features/CreateFeature";
import { ProductsContext } from "../products/ProductsContext";
import { ProductsTable } from "../products/ProductsTable";
import CreateProduct from "../products/CreateProduct";
import CreateAPIKey from "../developer/CreateAPIKey";
import { DevContext } from "../developer/DevContext";
import { CodeDisplay } from "@/components/general/CodeDisplay";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faBuilding,
faExternalLinkAlt,
} from "@fortawesome/pro-duotone-svg-icons";
let SUPABASE_URL = "https://tqjsbqmimvflvkwdoucx.supabase.co";
let SUPABASE_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InRxanNicW1pbXZmbHZrd2RvdWN4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzYyNjU0NTEsImV4cCI6MjA1MTg0MTQ1MX0.ndNu1-ObwQy5rzbmqQPvNRCG6z4GYkZKy_WkGo3AXNs";
function OnboardingView({ sessionClaims }: { sessionClaims: any }) {
const { org_id, user } = sessionClaims || {};
let supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
function OnboardingView({
sessionClaims,
env,
}: {
sessionClaims: any;
env: AppEnv;
}) {
const { org_id, user, org } = sessionClaims || {};
const searchParams = useSearchParams();
const { createOrganization, setActive } = useOrganizationList();
const [loading, setLoading] = useState(false);
const router = useRouter();
const [isExploding, setIsExploding] = useState(false);
const [orgId, setOrgId] = useState(org_id);
const [apiKeyName, setApiKeyName] = useState("");
const [apiCreated, setApiCreated] = useState(false);
//get features for the org
const {
data,
isLoading,
error,
mutate: featuresMutate,
} = useAxiosSWR({
url: `/features`,
env: env,
withAuth: true,
});
const features = data?.features.filter(
(feature: Feature) => feature.type !== "credit_system"
);
//get products for the org
const {
data: productData,
mutate: productMutate,
isLoading: productLoading,
} = useAxiosSWR({
url: `/products/data`,
env: env,
withAuth: true,
});
//supabase realtime to update products and features when org is created
useEffect(() => {
if (!orgId) return;
supabase
.channel(orgId)
.on(
"broadcast",
{
event: "org.created",
},
async (payload) => {
console.log("org created");
await featuresMutate();
await productMutate();
}
)
.subscribe();
}, [orgId, featuresMutate, productMutate]);
useEffect(() => {
const toastMessage = searchParams.get("toast");
if (toastMessage) {
toast.error(toastMessage);
}
}, [searchParams]);
const handleCreateOrg = async () => {
setLoading(true);
try {
if (!createOrganization) {
toast.error("Error creating organization");
return;
}
await createOrganization({ name: fields.name, slug: fields.slug });
await setActive({ organization: fields.slug });
window.location.href = '/sandbox/features';
const org = await createOrganization({
name: fields.name,
});
setOrgId(org.id);
console.log("org id", org.id);
await setActive({ organization: org.id });
toast.success(`Created your organization: ${org.name}`);
// window.location.href = "/sandbox/products";
setIsExploding(true);
} catch (error: any) {
if (error.message) {
toast.error(error.message);
@@ -41,35 +134,74 @@ function OnboardingView({ sessionClaims }: { sessionClaims: any }) {
}
}
setLoading(false);
if (env !== AppEnv.Sandbox) window.location.href = "/sandbox/onboarding";
};
const [slugEditted, setSlugEditted] = useState(false);
// const [slugEditted, setSlugEditted] = useState(false);
const [fields, setFields] = useState({
name: "",
name: org?.name || "",
slug: "",
});
if (!org_id && Object.keys(user.organizations).length == 0) {
return (
<div className="flex flex-col items-center justify-center h-screen w-screen bg-stone-50">
<CustomToaster />
<div className="w-[430px] shadow-lg rounded-2xl border flex flex-col p-8 bg-white gap-4">
<p className="text-lg font-bold text-t2">Create an organization</p>
<div>
{/* <FieldLabel>Name</FieldLabel> */}
<Input
placeholder="Your unique Organization Name"
value={fields.name}
onChange={(e) => {
const newFields = { ...fields, name: e.target.value };
if (!slugEditted) {
newFields.slug = slugify(e.target.value);
return (
<>
<CustomToaster />
<div className="flex flex-col p-8">
<Step title="Create your organization">
<div className="flex gap-8 w-full justify-between flex-col lg:flex-row">
<div className="text-t2 flex flex-col gap-2 w-full lg:w-1/3">
<p className="flex items-center">
<span>👋</span>
<span className="font-bold bg-gradient-to-r from-orange-500 via-pink-500 to-primary w-fit bg-clip-text text-transparent">
&nbsp; Welcome to Autumn
</span>
</p>
<p>
Create an organization to get started and integrate pricing
within 5 minutes.
</p>
</div>
<div className="w-full lg:w-2/3 min-w-md max-w-xl flex gap-2 bg-white p-4 rounded-sm border">
<Input
placeholder="Org name"
value={org?.name || fields.name}
disabled={!!org?.name}
onChange={(e) => {
const newFields = { ...fields, name: e.target.value };
setFields(newFields);
// if (!slugEditted) {
// newFields.slug = slugify(e.target.value);
// }
}}
/>
<Button
className="w-fit"
disabled={!!org?.name}
onClick={handleCreateOrg}
isLoading={loading}
variant="gradientPrimary"
startIcon={
<FontAwesomeIcon icon={faBuilding} className="mr-2" />
}
setFields(newFields);
}}
/>
>
Create Organization
</Button>
{isExploding && (
<ConfettiExplosion
force={0.8}
duration={3000}
particleCount={250}
zIndex={1000}
width={1600}
onComplete={() => {
console.log("complete");
}}
/>
)}
</div>
</div>
{/* <div>
</Step>
{/* <div>
<FieldLabel>Slug</FieldLabel>
<Input
placeholder="Organization Slug"
@@ -80,25 +212,218 @@ function OnboardingView({ sessionClaims }: { sessionClaims: any }) {
}}
/>
</div> */}
<div className="flex justify-end">
<Button
className="w-fit"
onClick={handleCreateOrg}
isLoading={loading}
variant="gradientPrimary"
>
Create Organization
</Button>
</div>
</div>
{org?.id && (
<>
<Step title="Connect your Stripe test account">
<div className="flex gap-8 w-full justify-between flex-col lg:flex-row">
<p className="text-t2 flex-col gap-2 w-full lg:w-1/3">
<span>
Paste in your{" "}
<a
className="text-primary underline font-semibold"
href="https://dashboard.stripe.com/test/apikeys"
target="_blank"
rel="noopener noreferrer"
>
Stripe Test Key
<FontAwesomeIcon
className="ml-1 h-2.5 w-2.5"
icon={faExternalLinkAlt}
/>
</a>{" "}
</span>
{/* <span>
You can add your live key later under the 'Connect to
Stripe' tab.
</span> */}
</p>
<ConnectStripe
className="w-full lg:w-2/3 min-w-md max-w-xl bg-white rounded-sm border p-6"
onboarding={true}
/>
</div>
</Step>
<Step title="Set up your pricing models">
<div className="flex gap-8 w-full justify-between flex-col lg:flex-row">
<div className="flex flex-col gap-2 text-t2 w-full lg:w-1/3">
<p>
<span className="font-bold">Features</span> are the benefits
your users are entitled to.{" "}
<span className="font-bold">Products</span> are how you
charge for them.
</p>
<p>
Define your own application features, and create the pricing
you want: subscriptions, usage-based, overages, credits, or
a a combination!
</p>
</div>
<div className="w-full lg:w-2/3 min-w-md max-w-xl flex flex-col gap-6">
<FeaturesContext.Provider
value={{
features: features,
dbConns: data?.dbConns,
env,
mutate: featuresMutate,
onboarding: true,
}}
>
{isLoading ? (
<SmallSpinner />
) : (
<div className="flex flex-col gap-2">
<p className="text-t2 font-medium text-md">Features</p>
<FeaturesTable />
<CreateFeature />
</div>
)}
</FeaturesContext.Provider>
<ProductsContext.Provider
value={{
...productData,
env,
mutate: productMutate,
onboarding: true,
}}
>
{productLoading ? (
<SmallSpinner />
) : (
<div className="flex flex-col gap-2">
<p className="text-t2 font-medium text-md">Products</p>
<ProductsTable products={productData?.products} />
<div>
<CreateProduct />
</div>
</div>
)}
</ProductsContext.Provider>
</div>
</div>
</Step>
<Step title="Create an Autumn API Key">
<div className="flex gap-8 w-full justify-between flex-col lg:flex-row">
<p className="text-t2 w-full lg:w-1/3">
Generate an API key to start integrating Autumn into your
application.
</p>
<div className="w-full lg:w-2/3 min-w-md max-w-xl flex gap-2 bg-white p-4 rounded-sm border">
<DevContext.Provider
value={{
env,
mutate: () => {},
onboarding: true,
apiKeyName,
setApiKeyName,
apiCreated,
setApiCreated,
}}
>
<Input
placeholder="API Key Name"
value={apiKeyName}
disabled={apiCreated}
onChange={(e) => setApiKeyName(e.target.value)}
/>
<CreateAPIKey />
</DevContext.Provider>
</div>
</div>
</Step>
<Step title="Attach a Product">
<div className="flex gap-8 w-full justify-between flex-col lg:flex-row">
<p className="flex flex-col gap-2 text-t2 w-full lg:w-1/3">
Call this endpoint when a user wants to purchase one of the
products we defined above.
<span>
Autumn will return a Stripe checkout URL that you should
redirect the user to.
</span>
</p>
<div className="w-full lg:w-2/3 min-w-md max-w-xl">
<CodeDisplay
code={`const response = await fetch('https://api.useautumn.com/v1/attach', {
method: "POST",
headers: {Authorization: 'Bearer <Autumn API Key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
"customer_id": internal_user_id, //Use your internal user ID
"product_id": "pro"
})
})`}
language="javascript"
/>
</div>
</div>
</Step>
<Step title="Check if user has access to a feature and send usage events">
<div className="flex gap-8 w-full justify-between flex-col lg:flex-row">
<p className="text-t2 flex flex-col gap-2 w-full lg:w-1/3">
<span>
Check whether a user has access to any of the features we
defined above.
</span>
<span>
If it's a metered (usage-based) feature, send us the usage
data.
</span>
</p>
<div className="w-full lg:w-2/3 min-w-md max-w-xl flex flex-col gap-2">
<h2 className="text-t2 font-medium text-md">Check Access</h2>
<CodeDisplay
code={`const response = await fetch('https://api.useautumn.com/v1/entitled', {
method: "POST",
headers: {Authorization: 'Bearer <Autumn API Key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
"customer_id": internal_user_id, //Use your internal user ID
"feature_id": "chat-messages"
})
})`}
language="javascript"
/>
<h2 className="text-t2 font-medium text-md mt-4">
Send Usage (if metered)
</h2>
<CodeDisplay
code={`await fetch('https://api.useautumn.com/v1/events', {
method: "POST",
headers: {Authorization: 'Bearer <Autumn API Key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
"customer_id": internal_user_id, //Use your internal user ID
"event_name": "chat-message"
})
})`}
language="javascript"
/>
</div>
</div>
</Step>
<Step title="Done!">
<div className="flex gap-8 w-full justify-between flex-col lg:flex-row">
<p className="text-t2 gap-2 w-full lg:w-1/3">
You're all set! Go to the Customers tab to manage your users,
and read our{" "}
<a
className="text-primary underline font-semibold break-none"
href="https://docs.useautumn.com"
target="_blank"
rel="noopener noreferrer"
>
Documentation
<FontAwesomeIcon
className="ml-1 h-2.5 w-2.5"
icon={faExternalLinkAlt}
/>
</a>{" "}
to learn more about what you can do with Autumn.
</p>
</div>
</Step>
</>
)}
</div>
);
} else
return (
<div className="flex flex-col items-center justify-center h-screen w-screen bg-stone-50">
<OrganizationList hidePersonal={true} />
</div>
);
</>
);
}
export default OnboardingView;

View File

@@ -114,7 +114,7 @@ function CreateProduct() {
setFields({ ...fields, is_add_on: e as boolean })
}
/>
<p className="mt-[1px]">This product is an add on</p>
<p className="mt-[1px]">This product is an add-on</p>
</div>
<div className="flex items-center gap-2 ml-1 text-t2">
<Checkbox
@@ -123,7 +123,9 @@ function CreateProduct() {
setFields({ ...fields, is_default: e as boolean })
}
/>
<p className="mt-[1px]">This product is the default product</p>
<p className="mt-[1px]">
Add this product to customers by default on creation
</p>
</div>
</div>

View File

@@ -20,7 +20,7 @@ import { useProductsContext } from "./ProductsContext";
import { Badge } from "@/components/ui/badge";
export const ProductsTable = ({ products }: { products: Product[] }) => {
const { env } = useProductsContext();
const { env, onboarding } = useProductsContext();
const router = useRouter();
return (
<Table>
@@ -29,9 +29,11 @@ export const ProductsTable = ({ products }: { products: Product[] }) => {
<TableHead className="">Name</TableHead>
<TableHead>Product ID</TableHead>
<TableHead>Type</TableHead>
<TableHead>Group</TableHead>
<TableHead>Created At</TableHead>
<TableHead></TableHead>
{!onboarding && <TableHead>Group</TableHead>}
{!onboarding && (
<TableHead className="min-w-0 w-28">Created At</TableHead>
)}
<TableHead className="min-w-0 w-10"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -41,12 +43,8 @@ export const ProductsTable = ({ products }: { products: Product[] }) => {
className="cursor-pointer"
onClick={() => navigateTo(`/products/${product.id}`, router, env)}
>
<TableCell className="font-medium">
{product.name}
</TableCell>
<TableCell className="font-mono">
{product.id}
</TableCell>
<TableCell className="font-medium">{product.name}</TableCell>
<TableCell className="font-mono">{product.id}</TableCell>
<TableCell className="min-w-32">
{product.is_default ? (
<Badge variant="outline">Default</Badge>
@@ -56,17 +54,16 @@ export const ProductsTable = ({ products }: { products: Product[] }) => {
<></>
)}
</TableCell>
<TableCell>{product.group}</TableCell>
<TableCell className="min-w-20 w-24">
<span>
{formatUnixToDateTime(product.created_at).date}
</span>
{" "}
<span className="text-t3">
{formatUnixToDateTime(product.created_at).time}
</span>
</TableCell>
<TableCell className="min-w-4 w-6">
{!onboarding && <TableCell>{product.group}</TableCell>}
{!onboarding && (
<TableCell>
<span>{formatUnixToDateTime(product.created_at).date}</span>{" "}
<span className="text-t3">
{formatUnixToDateTime(product.created_at).time}
</span>
</TableCell>
)}
<TableCell>
<ProductRowToolbar product={product} />
</TableCell>
</TableRow>

View File

@@ -23,6 +23,7 @@ import { EnvDropdown } from "./EnvDropdown";
import { AppEnv } from "@autumn/shared";
import SidebarBottom from "./SidebarBottom";
import { createClient } from "@supabase/supabase-js";
function HomeSidebar({
user,
@@ -40,8 +41,8 @@ function HomeSidebar({
}) {
return (
<Sidebar collapsible="icon" className=" bg-zinc-100">
<SidebarTop orgName={org.name} env={env} />
<SidebarContent >
<SidebarTop orgName={org.name || " "} env={env} />
<SidebarContent>
<SidebarGroup className="py-0">
<SidebarGroupContent>
<SidebarMenu>

View File

@@ -25,12 +25,10 @@ export function SidebarTop({ orgName, env }: { orgName: string; env: AppEnv }) {
const [hidePlaceholder, setHidePlaceholder] = useState(false);
useEffect(() => {
if (organization) {
setCurOrgId(organization.id);
}
if (organization) setCurOrgId(organization.id);
if (curOrgId !== null && organization?.id !== curOrgId) {
window.location.href = "/sandbox/customers";
if (organization && curOrgId !== null && organization?.id !== curOrgId) {
window.location.href = "/sandbox/products";
router.refresh();
}
}, [organization, curOrgId, router]);
@@ -65,11 +63,12 @@ export function SidebarTop({ orgName, env }: { orgName: string; env: AppEnv }) {
{!isLoaded && <SmallSpinner />}
</div>
)}
{isLoaded && (
{isLoaded && organization && (
<OrganizationSwitcher
appearance={{
elements: {
organizationSwitcherTrigger: "pl-0 pr-1 max-w-[160px]",
organizationSwitcherTrigger:
"flex pl-0 pr-1 max-w-[160px]",
},
}}
hidePersonal={true}
@@ -92,7 +91,7 @@ export function SidebarTop({ orgName, env }: { orgName: string; env: AppEnv }) {
{/* <span className="w-[70px] overflow-hidden text-ellipsis whitespace-nowrap">
{organization?.id}
</span> */}
<CopyButton text={organization?.id || ""} />
<CopyButton text={organization?.id || ""} />
</div>
)}
</div>

View File

@@ -16,6 +16,7 @@ export const TabButton = ({
const path = usePathname();
const isActive = path.includes(value);
const router = useRouter();
return (
<SidebarMenuItem key={value}>
<SidebarMenuButton asChild>

View File

@@ -107,6 +107,13 @@ export default {
},
},
},
// eslint-disable-next-line @typescript-eslint/no-require-imports
plugins: [require("tailwindcss-animate"), nextui(nextUiConfig)],
plugins: [
require("tailwindcss-animate"),
nextui(nextUiConfig),
function ({ addBase, theme }) {
addBase({
body: { fontSize: theme("fontSize.sm") },
});
},
],
} satisfies Config;

450
package-lock.json generated
View File

@@ -43,7 +43,7 @@
"@supabase/ssr": "^0.5.2",
"@supabase/supabase-js": "^2.47.2",
"@useautumn/react": "latest",
"@wooorm/starry-night": "^3.5.0",
"@wooorm/starry-night": "^3.6.0",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -55,14 +55,17 @@
"lodash": "^4.17.21",
"lucide-react": "^0.468.0",
"next": "15.0.3",
"next-themes": "^0.4.4",
"ora": "^8.1.1",
"pointer-sdk": "^0.0.9",
"react": "^18.2.0",
"react-confetti-explosion": "^2.1.2",
"react-day-picker": "^8.10.1",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.6.1",
"react-use-websocket": "^4.13.0",
"sonner": "^1.7.4",
"swr": "^2.2.5",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
@@ -5123,20 +5126,6 @@
"url": "https://opencollective.com/eslint"
}
},
"frontend/node_modules/@wooorm/starry-night": {
"version": "3.5.0",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"import-meta-resolve": "^4.0.0",
"vscode-oniguruma": "^2.0.0",
"vscode-textmate": "^9.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"frontend/node_modules/acorn-jsx": {
"version": "5.3.2",
"dev": true,
@@ -6460,14 +6449,6 @@
"node": ">= 4"
}
},
"frontend/node_modules/import-meta-resolve": {
"version": "4.1.0",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"frontend/node_modules/imurmurhash": {
"version": "0.1.4",
"dev": true,
@@ -7046,13 +7027,6 @@
"dev": true,
"license": "MIT"
},
"frontend/node_modules/object-assign": {
"version": "4.1.1",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"frontend/node_modules/object-hash": {
"version": "3.0.0",
"license": "MIT",
@@ -7394,15 +7368,6 @@
"node": ">= 0.8.0"
}
},
"frontend/node_modules/prop-types": {
"version": "15.8.1",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"frontend/node_modules/queue-microtask": {
"version": "1.2.3",
"funding": [
@@ -7457,10 +7422,6 @@
"react-dom": ">=16.8.1"
}
},
"frontend/node_modules/react-is": {
"version": "16.13.1",
"license": "MIT"
},
"frontend/node_modules/react-remove-scroll": {
"version": "2.6.0",
"license": "MIT",
@@ -8197,14 +8158,6 @@
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0"
}
},
"frontend/node_modules/vscode-oniguruma": {
"version": "2.0.1",
"license": "MIT"
},
"frontend/node_modules/vscode-textmate": {
"version": "9.1.0",
"license": "MIT"
},
"frontend/node_modules/which-boxed-primitive": {
"version": "1.1.0",
"dev": true,
@@ -11354,6 +11307,22 @@
"react-dom": "^18.2.0"
}
},
"node_modules/@wooorm/starry-night": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@wooorm/starry-night/-/starry-night-3.6.0.tgz",
"integrity": "sha512-AoDrqWZCZVymY48BClDaSuLgOe5vnl99S4hrqqg5OJ44x9NHo4s5LzOypKC3e6jGIUqHDfaDMvou8pelkq3dCw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"import-meta-resolve": "^4.0.0",
"vscode-oniguruma": "^2.0.0",
"vscode-textmate": "^9.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/abbrev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
@@ -12637,6 +12606,27 @@
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/css-jss": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/css-jss/-/css-jss-10.10.0.tgz",
"integrity": "sha512-YyMIS/LsSKEGXEaVJdjonWe18p4vXLo8CMA4FrW/kcaEyqdIGKCFXao31gbJddXEdIxSXFFURWrenBJPlKTgAA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"jss": "^10.10.0",
"jss-preset-default": "^10.10.0"
}
},
"node_modules/css-vendor": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz",
"integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.3",
"is-in-browser": "^1.0.2"
}
},
"node_modules/csstype": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
@@ -13902,6 +13892,15 @@
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
"license": "CC0-1.0"
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/html-to-text": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
@@ -13996,6 +13995,12 @@
"integrity": "sha512-inh5wue5XdfObhu/IGEMiA1nUXigSGcaKNemcbLRKa7jXYGDZXr3LoT9pTIzq2hPEbld7w/qv9h+ikWGz8fL1g==",
"license": "Unlicense"
},
"node_modules/hyphenate-style-name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
"license": "BSD-3-Clause"
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -14035,6 +14040,16 @@
"module-details-from-path": "^1.0.3"
}
},
"node_modules/import-meta-resolve": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
"integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -14299,6 +14314,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-in-browser": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
"integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==",
"license": "MIT"
},
"node_modules/is-ip": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.1.tgz",
@@ -14522,6 +14543,172 @@
"node": ">=6"
}
},
"node_modules/jss": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz",
"integrity": "sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"csstype": "^3.0.2",
"is-in-browser": "^1.1.3",
"tiny-warning": "^1.0.2"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/jss"
}
},
"node_modules/jss-plugin-camel-case": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz",
"integrity": "sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"hyphenate-style-name": "^1.0.3",
"jss": "10.10.0"
}
},
"node_modules/jss-plugin-compose": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss-plugin-compose/-/jss-plugin-compose-10.10.0.tgz",
"integrity": "sha512-F5kgtWpI2XfZ3Z8eP78tZEYFdgTIbpA/TMuX3a8vwrNolYtN1N4qJR/Ob0LAsqIwCMLojtxN7c7Oo/+Vz6THow==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"jss": "10.10.0",
"tiny-warning": "^1.0.2"
}
},
"node_modules/jss-plugin-default-unit": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz",
"integrity": "sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"jss": "10.10.0"
}
},
"node_modules/jss-plugin-expand": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss-plugin-expand/-/jss-plugin-expand-10.10.0.tgz",
"integrity": "sha512-ymT62W2OyDxBxr7A6JR87vVX9vTq2ep5jZLIdUSusfBIEENLdkkc0lL/Xaq8W9s3opUq7R0sZQpzRWELrfVYzA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"jss": "10.10.0"
}
},
"node_modules/jss-plugin-extend": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss-plugin-extend/-/jss-plugin-extend-10.10.0.tgz",
"integrity": "sha512-sKYrcMfr4xxigmIwqTjxNcHwXJIfvhvjTNxF+Tbc1NmNdyspGW47Ey6sGH8BcQ4FFQhLXctpWCQSpDwdNmXSwg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"jss": "10.10.0",
"tiny-warning": "^1.0.2"
}
},
"node_modules/jss-plugin-global": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz",
"integrity": "sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"jss": "10.10.0"
}
},
"node_modules/jss-plugin-nested": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz",
"integrity": "sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"jss": "10.10.0",
"tiny-warning": "^1.0.2"
}
},
"node_modules/jss-plugin-props-sort": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz",
"integrity": "sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"jss": "10.10.0"
}
},
"node_modules/jss-plugin-rule-value-function": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz",
"integrity": "sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"jss": "10.10.0",
"tiny-warning": "^1.0.2"
}
},
"node_modules/jss-plugin-rule-value-observable": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss-plugin-rule-value-observable/-/jss-plugin-rule-value-observable-10.10.0.tgz",
"integrity": "sha512-ZLMaYrR3QE+vD7nl3oNXuj79VZl9Kp8/u6A1IbTPDcuOu8b56cFdWRZNZ0vNr8jHewooEeq2doy8Oxtymr2ZPA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"jss": "10.10.0",
"symbol-observable": "^1.2.0"
}
},
"node_modules/jss-plugin-template": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss-plugin-template/-/jss-plugin-template-10.10.0.tgz",
"integrity": "sha512-ocXZBIOJOA+jISPdsgkTs8wwpK6UbsvtZK5JI7VUggTD6LWKbtoxUzadd2TpfF+lEtlhUmMsCkTRNkITdPKa6w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"jss": "10.10.0",
"tiny-warning": "^1.0.2"
}
},
"node_modules/jss-plugin-vendor-prefixer": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz",
"integrity": "sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"css-vendor": "^2.0.8",
"jss": "10.10.0"
}
},
"node_modules/jss-preset-default": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/jss-preset-default/-/jss-preset-default-10.10.0.tgz",
"integrity": "sha512-GL175Wt2FGhjE+f+Y3aWh+JioL06/QWFgZp53CbNNq6ZkVU0TDplD8Bxm9KnkotAYn3FlplNqoW5CjyLXcoJ7Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"jss": "10.10.0",
"jss-plugin-camel-case": "10.10.0",
"jss-plugin-compose": "10.10.0",
"jss-plugin-default-unit": "10.10.0",
"jss-plugin-expand": "10.10.0",
"jss-plugin-extend": "10.10.0",
"jss-plugin-global": "10.10.0",
"jss-plugin-nested": "10.10.0",
"jss-plugin-props-sort": "10.10.0",
"jss-plugin-rule-value-function": "10.10.0",
"jss-plugin-rule-value-observable": "10.10.0",
"jss-plugin-template": "10.10.0",
"jss-plugin-vendor-prefixer": "10.10.0"
}
},
"node_modules/ksuid": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ksuid/-/ksuid-3.0.0.tgz",
@@ -16148,6 +16335,16 @@
}
}
},
"node_modules/next-themes": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
"integrity": "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
@@ -16238,6 +16435,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
@@ -16860,6 +17066,17 @@
"node": ">=0.4.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/property-information": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
@@ -17045,6 +17262,26 @@
"node": ">=0.10.0"
}
},
"node_modules/react-confetti-explosion": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/react-confetti-explosion/-/react-confetti-explosion-2.1.2.tgz",
"integrity": "sha512-4UzDFBajAGXmF9TSJoRMO2QOBCIXc66idTxH8l7Mkul48HLGtk+tMzK9HYDYsy7Zmw5sEGchi2fbn4AJUuLrZw==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.21",
"react-jss": "^10.9.2"
},
"peerDependencies": {
"react": "^18.x",
"react-dom": "^18.x"
}
},
"node_modules/react-display-name": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/react-display-name/-/react-display-name-0.2.5.tgz",
"integrity": "sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==",
"license": "MIT"
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -17058,6 +17295,49 @@
"react": "^18.3.1"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-jss": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/react-jss/-/react-jss-10.10.0.tgz",
"integrity": "sha512-WLiq84UYWqNBF6579/uprcIUnM1TSywYq6AIjKTTTG5ziJl9Uy+pwuvpN3apuyVwflMbD60PraeTKT7uWH9XEQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"@emotion/is-prop-valid": "^0.7.3",
"css-jss": "10.10.0",
"hoist-non-react-statics": "^3.2.0",
"is-in-browser": "^1.1.3",
"jss": "10.10.0",
"jss-preset-default": "10.10.0",
"prop-types": "^15.6.0",
"shallow-equal": "^1.2.0",
"theming": "^3.3.0",
"tiny-warning": "^1.0.2"
},
"peerDependencies": {
"react": ">=16.8.6"
}
},
"node_modules/react-jss/node_modules/@emotion/is-prop-valid": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.7.3.tgz",
"integrity": "sha512-uxJqm/sqwXw3YPA5GXX365OBcJGFtxUVkB6WyezqFHlNe9jqUWH5ur2O2M8dGBz61kn1g3ZBlzUunFQXQIClhA==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "0.7.1"
}
},
"node_modules/react-jss/node_modules/@emotion/memoize": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.1.tgz",
"integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==",
"license": "MIT"
},
"node_modules/react-markdown": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.3.tgz",
@@ -17572,6 +17852,12 @@
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
"license": "MIT"
},
"node_modules/shallow-equal": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==",
"license": "MIT"
},
"node_modules/sharp": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
@@ -17874,6 +18160,16 @@
"atomic-sleep": "^1.0.0"
}
},
"node_modules/sonner": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
"integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -18218,6 +18514,15 @@
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tailwind-merge": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.1.tgz",
@@ -18296,6 +18601,24 @@
"b4a": "^1.6.4"
}
},
"node_modules/theming": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/theming/-/theming-3.3.0.tgz",
"integrity": "sha512-u6l4qTJRDaWZsqa8JugaNt7Xd8PPl9+gonZaIe28vAhqgHMIG/DOyFPqiKN/gQLQYj05tHv+YQdNILL4zoiAVA==",
"license": "MIT",
"dependencies": {
"hoist-non-react-statics": "^3.3.0",
"prop-types": "^15.5.8",
"react-display-name": "^0.2.4",
"tiny-warning": "^1.0.2"
},
"engines": {
"node": ">=8"
},
"peerDependencies": {
"react": ">=16.3"
}
},
"node_modules/thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
@@ -18320,6 +18643,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
"license": "MIT"
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -18712,6 +19041,18 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/vscode-oniguruma": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-2.0.1.tgz",
"integrity": "sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ==",
"license": "MIT"
},
"node_modules/vscode-textmate": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.2.0.tgz",
"integrity": "sha512-rkvG4SraZQaPSN/5XjwKswdU0OP9MF28QjrYzUBbhb8QyG3ljB1Ky996m++jiI7KdiAP2CkBiQZd9pqEDTClqA==",
"license": "MIT"
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -20513,13 +20854,6 @@
"version": "2.1.3",
"license": "MIT"
},
"server/node_modules/object-assign": {
"version": "4.1.1",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"server/node_modules/on-finished": {
"version": "2.4.1",
"license": "MIT",

View File

@@ -15,6 +15,13 @@ import { AppEnv } from "@autumn/shared";
export const orgRouter = express.Router();
orgRouter.get("", async (req: any, res) => {
if (!req.orgId) {
res.status(400).json({
message: "Missing orgId",
});
return;
}
const org = await OrgService.getFullOrg({
sb: req.sb,
orgId: req.orgId,