feat: cleaning up ai tools

This commit is contained in:
John Yeo
2025-08-03 19:51:51 -07:00
parent d50a390d26
commit 00ea760edc
32 changed files with 997 additions and 228 deletions

View File

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" data-testid="geist-icon" height="16" stroke-linejoin="round" style="color:currentColor" viewBox="0 0 16 16" width="16"><g clip-path="url(#clip0_53_108)">
<circle cx="8" cy="8" r="7.375" fill="black" stroke="var(--ds-gray-1000)" strokewidth="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.63 11V5" stroke="url(#paint0_linear_53_108vsxrmxu21)" strokewidth="1.25" stroke-miterlimit="1.41421"/>
<path fill-rule="evenodd" cliprule="evenodd" d="M5.995 5.00087V5H4.745V11H5.995V6.96798L12.3615 14.7076C12.712 14.4793 13.0434 14.2242 13.353 13.9453L5.99527 5.00065L5.995 5.00087Z" fill="url(#paint1_linear_53_108vsxrmxu21)"/>
</g>
<defs>
<linearGradient id="paint0_linear_53_108vsxrmxu21" x1="11.13" y1="5" x2="11.13" y2="11" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="0.609375" stop-color="white" stop-opacity="0.57"/>
<stop offset="0.796875" stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_53_108vsxrmxu21" x1="9.9375" y1="9.0625" x2="13.5574" y2="13.3992" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_53_108">
<rect width="16" height="16" fill="red"/>
</clipPath>
</defs></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1,6 @@
<svg width="602" height="360" viewBox="0 0 602 360" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M481.36 180C481.36 196.572 474.639 211.572 463.757 222.42C452.875 233.28 437.845 240 421.24 240C404.635 240 389.605 246.708 378.735 257.568C367.853 268.428 361.12 283.428 361.12 300C361.12 316.572 354.399 331.572 343.517 342.42C332.635 353.28 317.605 360 301 360C284.395 360 269.365 353.28 258.495 342.42C247.613 331.572 240.88 316.572 240.88 300C240.88 283.428 247.613 268.428 258.495 257.568C269.365 246.708 284.395 240 301 240C317.605 240 332.635 233.28 343.517 222.42C354.399 211.572 361.12 196.572 361.12 180C361.12 146.856 334.21 120 301 120C284.395 120 269.365 113.28 258.495 102.42C247.613 91.572 240.88 76.572 240.88 60C240.88 43.428 247.613 28.428 258.495 17.568C269.365 6.708 284.395 0 301 0C334.21 0 361.12 26.856 361.12 60C361.12 76.572 367.853 91.572 378.735 102.42C389.605 113.28 404.635 120 421.24 120C454.45 120 481.36 146.856 481.36 180Z" fill="#F44250"/>
<path d="M240.88 180C240.88 146.862 213.964 120 180.76 120C147.557 120 120.64 146.862 120.64 180C120.64 213.137 147.557 240 180.76 240C213.964 240 240.88 213.137 240.88 180Z" fill="#121212"/>
<path d="M120.64 300C120.64 266.863 93.7234 240 60.52 240C27.3167 240 0.400024 266.863 0.400024 300C0.400024 333.138 27.3167 360 60.52 360C93.7234 360 120.64 333.138 120.64 300Z" fill="#121212"/>
<path d="M601.6 300C601.6 266.863 574.683 240 541.48 240C508.277 240 481.36 266.863 481.36 300C481.36 333.138 508.277 360 541.48 360C574.683 360 601.6 333.138 601.6 300Z" fill="#121212"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -0,0 +1,15 @@
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -24,13 +24,17 @@ TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> & {
variant?: "default" | "onboarding";
}
>(({ className, variant = "default", ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-2 py-1 text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-primary hover:bg-stone-100 dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300 dark:data-[state=active]:bg-zinc-950 dark:data-[state=active]:text-zinc-50",
className
className,
variant === "onboarding" &&
"data-[state=active]:bg-stone-200 data-[state=active]:text-t2 data-[state=active]:font-medium"
)}
{...props}
/>

View File

@@ -21,13 +21,13 @@ export const CreateSecretKey = ({
setApiKey: (apiKey: string) => void;
number: number;
}) => {
let env = useEnv();
let [apiKeyName, setApiKeyName] = useState("");
let [apiCreated, setApiCreated] = useState(false);
const env = useEnv();
const [apiKeyName, setApiKeyName] = useState("");
const [apiCreated, setApiCreated] = useState(false);
const [loading, setLoading] = useState(false);
const [copied, setCopied] = useState(false);
let axiosInstance = useAxiosInstance({ env });
const axiosInstance = useAxiosInstance({ env });
const handleCreate = async () => {
console.log("creating api key", apiKeyName ? apiKeyName : name);

View File

@@ -1,8 +1,15 @@
import { StepHeader } from "./StepHeader";
import { Button } from "@/components/ui/button";
import CopyButton from "@/components/general/CopyButton";
import { ExternalLink, Download } from "lucide-react";
import { ExternalLink, Download, Info } from "lucide-react";
import { toast } from "sonner";
import {
Accordion,
AccordionTrigger,
AccordionContent,
AccordionItem,
} from "@/components/ui/accordion";
import FieldLabel from "@/components/general/modal-components/FieldLabel";
export const AITools = () => {
// MCP configuration for Autumn
@@ -33,7 +40,41 @@ export const AITools = () => {
return (
<div className="flex flex-col gap-2">
<StepHeader number={1} title="Add to AI tools" />
{/* <StepHeader number={1} title="Add to AI tools" /> */}
<Accordion
type="single"
collapsible
className="w-full"
defaultValue="item-1"
>
<AccordionItem value="item-1" className="border">
<AccordionTrigger className="bg-stone-100 p-2">
<div className="flex items-center gap-2 text-t2">
<Info size={14} />
Add Autumn to your AI tools
</div>
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance text-t3 p-4 bg-white">
<div>
<FieldLabel>Cursor</FieldLabel>
<Button
onClick={handleCursorInstall}
variant="outline"
className="w-fit border-none bg-zinc-800 hover:bg-zinc-900 text-white rounded-xs hover:text-white"
startIcon={
<img
src="/cursor.png"
className="w-4 h-4 mr-1"
alt="Cursor"
/>
}
>
Open in Cursor
</Button>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<p className="text-sm text-t3">
If you're using Cursor or Claude Code, you can install our MCP server to
use AI to integrate Autumn.
@@ -41,16 +82,6 @@ export const AITools = () => {
{/* One-click install for Cursor */}
<div className="flex gap-2">
<Button
onClick={handleCursorInstall}
variant="outline"
className="w-fit text-t2"
startIcon={
<img src="/cursor.png" className="w-4 h-4 mr-1" alt="Cursor" />
}
>
Open in Cursor
</Button>
<Button
onClick={() => {
navigator.clipboard.writeText(

View File

@@ -1,38 +1,35 @@
import { AITools } from "./AITools";
import { SelectStack } from "./SelectStack";
import { useState } from "react";
import { IntegrateContext } from "./IntegrateContext";
import { notNullish } from "@/utils/genUtils";
import { Install } from "./integration-steps/Install";
import { AutumnHandler } from "./integration-steps/AutumnHandler";
import { parseAsString, parseAsJson, useQueryStates } from "nuqs";
import {
parseAsString,
parseAsJson,
useQueryStates,
parseAsBoolean,
} from "nuqs";
import { AddAutumnProvider } from "./integration-steps/AddAutumnProvider";
import { CheckoutPricingTable } from "./integration-steps/CheckoutPricingTable";
import { EnvStep } from "./integration-steps/EnvStep";
export default function IntegrateAutumn() {
const [stack, setStack] = useState<{
frontend: string;
backend: string;
auth: string;
customerType: string;
}>({
frontend: "",
backend: "",
auth: "",
customerType: "",
});
const [queryStates, setQueryStates] = useQueryStates({
reactTypescript: parseAsBoolean.withDefault(true),
frontend: parseAsString.withDefault(""),
backend: parseAsString.withDefault(""),
auth: parseAsString.withDefault(""),
customerType: parseAsString.withDefault(""),
});
const stackSelected = Object.values(stack).every(notNullish);
const stackSelected = Object.values(queryStates).every(notNullish);
return (
<IntegrateContext.Provider value={{ queryStates, setQueryStates }}>
<div className="w-full h-full p-10 flex flex-col items-center justify-start">
<div className="max-w-[700px] w-full flex flex-col gap-6">
<div className="max-w-[600px] w-full flex flex-col gap-6">
<div className="flex flex-col gap-2">
<p className="text-xl">Integrate Autumn</p>
<p className="text-t3">
@@ -44,13 +41,17 @@ export default function IntegrateAutumn() {
<div className="flex flex-col gap-8 pb-40">
<AITools />
<SelectStack />
{stackSelected && (
{stackSelected && queryStates.reactTypescript && (
<>
<EnvStep />
<Install />
<AutumnHandler />
<AddAutumnProvider />
<CheckoutPricingTable />
</>
)}
</div>
{/* <div className="flex flex-col gap-4">
<StepHeader number={2} title="Add your secret key" />
<p className="text-md text-t3">

View File

@@ -8,176 +8,78 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useIntegrateContext } from "./IntegrateContext";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Building,
CircleUserRound,
Code,
Fingerprint,
Info,
InfoIcon,
ScanFace,
User,
} from "lucide-react";
import { SelectFrameworks } from "./select-stack/SelectFrameworks";
import { InfoBox } from "./components/InfoBox";
import { Button } from "@/components/ui/button";
export const SelectStack = () => {
const { queryStates, setQueryStates } = useIntegrateContext();
const frontendOptions = [
{ value: "nextjs", label: "Next.js" },
{ value: "vite", label: "Vite SPA" },
{ value: "tanstack", label: "Tanstack Start" },
{ value: "non-react", label: "Non-React" },
];
const backendOptions = [
{ value: "nextjs", label: "Next.js" },
{ value: "react_router", label: "React Router 7" },
{ value: "hono", label: "Hono" },
{ value: "express", label: "Express" },
{ value: "elysia", label: "Elysia" },
{ value: "supabase", label: "Supabase" },
{ value: "convex", label: "Convex" },
];
const authOptions = [
{ value: "better_auth", label: "Better Auth" },
{ value: "supabase", label: "Supabase Auth" },
{ value: "clerk", label: "Clerk" },
{ value: "other", label: "Other" },
];
const customerOptions = [
{ value: "user", label: "Users" },
{ value: "org", label: "Organizations" },
// { value: "other", label: "Other (eg. Projects / Workspaces)" },
];
const tabClassName = `rounded-xs h-8 data-[state=active]:bg-stone-100 data-[state=active]:text-t2 data-[state=active]:shadow-inner data-[state=active]:border`;
return (
<div className="flex flex-col gap-4">
<StepHeader number={2} title="Select your stack" />
<p className="text-sm text-t3">
Help us customize the integration guide for your specific tech stack.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-[600px]">
{/* Frontend Framework */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-t2">Frontend</label>
<Select
value={queryStates.frontend}
onValueChange={(value) =>
setQueryStates({ ...queryStates, frontend: value })
}
<div className="flex flex-col gap-2">
<StepHeader number={2} title="Select your stack" />
<p className="text-sm text-t3">
Help us customize the integration guide for your specific tech stack.
Click{" "}
<span
onClick={() => {
setQueryStates({ ...queryStates, reactTypescript: false });
}}
className="underline cursor-pointer"
>
<SelectTrigger>
<SelectValue placeholder="Select framework" />
</SelectTrigger>
<SelectContent>
{frontendOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Backend Framework */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-t2">Backend</label>
<Select
value={queryStates.backend}
onValueChange={(value) =>
setQueryStates({ ...queryStates, backend: value })
}
>
<SelectTrigger>
<SelectValue placeholder="Select backend" />
</SelectTrigger>
<SelectContent>
{backendOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Auth Provider */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-t2">Auth Provider</label>
<Select
value={queryStates.auth}
onValueChange={(value) =>
setQueryStates({ ...queryStates, auth: value })
}
>
<SelectTrigger>
<SelectValue placeholder="Select auth provider" />
</SelectTrigger>
<SelectContent>
{authOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Customer Type */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-t2">
Your customers are
</label>
<Select
value={queryStates.customerType}
onValueChange={(value) =>
setQueryStates({ ...queryStates, customerType: value })
}
>
<SelectTrigger>
<SelectValue placeholder="Select customer type" />
</SelectTrigger>
<SelectContent>
{customerOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
here
</span>{" "}
if you're not using a React + Typescript backend stack.
</p>
</div>
{/* {(stack.frontend ||
stack.backend ||
stack.auth ||
stack.customerType) && (
<div className="mt-4 p-3 bg-zinc-50 dark:bg-zinc-900 rounded-md border border-zinc-200 dark:border-zinc-800">
<h4 className="text-sm font-medium text-t2 mb-2">Selected Stack:</h4>
<div className="text-xs text-t3 space-y-1">
{stack.frontend && (
<p>
<strong>Frontend:</strong>{" "}
{frontendOptions.find((f) => f.value === stack.frontend)?.label}
</p>
)}
{stack.backend && (
<p>
<strong>Backend:</strong>{" "}
{backendOptions.find((b) => b.value === stack.backend)?.label}
</p>
)}
{stack.auth && (
<p>
<strong>Auth:</strong>{" "}
{authOptions.find((a) => a.value === stack.auth)?.label}
</p>
)}
{stack.customerType && (
<p>
<strong>Customers:</strong>{" "}
{
customerOptions.find((c) => c.value === stack.customerType)
?.label
}
</p>
)}
</div>
</div>
)} */}
{queryStates.reactTypescript ? (
<SelectFrameworks />
) : (
<>
<InfoBox>
<p className="text-t2 text-sm">
This onboarding guide shows how to set up Autumn using our
frontend components / hooks on React and server-side framework
adaptors.
<br />
<br />
In your case, you should integrate with Autumn's API directly on
the backend. We have SDKs for Typescript and Python. Learn how to
do so{" "}
<a
className="text-t2 font-medium underline"
href="https://docs.useautumn.com/setup/backend"
target="_blank"
>
here
</a>
.
</p>
</InfoBox>
<Button
variant="outline"
className="w-fit"
onClick={() => {
setQueryStates({ ...queryStates, reactTypescript: true });
}}
>
Learn how to integrate Autumn with React + Typescript
</Button>
</>
)}
</div>
);
};

View File

@@ -0,0 +1,15 @@
export enum Backend {
Nextjs = "nextjs",
ReactRouter = "react-router",
Express = "express",
Elysia = "elysia",
Hono = "hono",
Other = "other",
}
export enum Frontend {
Nextjs = "nextjs",
ReactRouter = "react-router",
Vite = "vite",
Other = "other",
}

View File

@@ -3,14 +3,14 @@ export const StepHeader = ({
title,
}: {
number: number;
title: string;
title: React.ReactNode;
}) => {
return (
<div className="flex items-center gap-4">
<div className="w-6 h-6 border-1 rounded-full bg-gradient-to-b from-stone-100 to-stone-100 text-primary font-bold flex items-center justify-center text-sm">
{number}
</div>
<p className="text-md">{title}</p>
<div className="text-md">{title}</div>
</div>
);
};

View File

@@ -0,0 +1,7 @@
export const CodeSpan = ({ children }: { children: React.ReactNode }) => {
return (
<span className="bg-stone-200 font-mono text-t2 px-1 py-0.5 rounded-md">
{children}
</span>
);
};

View File

@@ -0,0 +1,27 @@
import { cn } from "@/lib/utils";
import { Info } from "lucide-react";
export const InfoBox = ({
classNames,
children,
}: {
classNames?: {
infoIcon?: string;
infoBox?: string;
};
children: React.ReactNode;
}) => {
return (
<div
className={cn(
"bg-stone-100 border p-4 text-t2 flex gap-2",
classNames?.infoBox
)}
>
<div className={cn("pt-0.5 mr-1", classNames?.infoIcon)}>
<Info size={14} />
</div>
{children}
</div>
);
};

View File

@@ -0,0 +1,162 @@
import CodeBlock from "@/views/onboarding/components/CodeBlock";
import { CodeSpan } from "../components/CodeSpan";
import { StepHeader } from "../StepHeader";
import { useIntegrateContext } from "../IntegrateContext";
import { Backend, Frontend } from "../StackEnums";
import { InfoBox } from "../components/InfoBox";
const nextjsAutumnProvider = ({
includeBackendUrl,
}: {
includeBackendUrl: boolean;
}) => {
const backendUrlStr = includeBackendUrl
? ` backendUrl={process.env.NEXT_PUBLIC_BACKEND_URL}`
: "";
return `// layout.tsx
import { AutumnProvider, PricingTable } from "autumn-js/react";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<AutumnProvider${backendUrlStr}>
{children}
</AutumnProvider>
</body>
</html>
);
}`;
};
const rr7AutumnProvider = ({
includeBackendUrl,
}: {
includeBackendUrl: boolean;
}) => {
const backendUrlStr = includeBackendUrl
? ` backendUrl={import.meta.env.VITE_BACKEND_URL}`
: "";
return `// root.tsx
import { AutumnProvider } from "autumn-js/react";
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<AutumnProvider${backendUrlStr}>{children}</AutumnProvider>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}`;
};
const viteAutumnProvider = () => {
return `// main.tsx
import { AutumnProvider } from "autumn-js/react";
createRoot(document.getElementById("root")!).render(
<AutumnProvider backendUrl={import.meta.env.VITE_BACKEND_URL}>
<App />
</AutumnProvider>
);`;
};
const otherAutumnProvider = () => {
return `// main.tsx
import { AutumnProvider } from "autumn-js/react";
// 1. Simply wrap the root of your app in the AutumnProvider component
// 2. Pass in your server's URL to the backendUrl prop
createRoot(document.getElementById("root")!).render(
<Layout>
<AutumnProvider backendUrl={import.meta.env.VITE_BACKEND_URL}>
<App />
</AutumnProvider>
</Layout>
);`;
};
const getSnippet = (queryStates: any) => {
const { backend, frontend } = queryStates;
if (frontend === Frontend.Nextjs) {
return nextjsAutumnProvider({
includeBackendUrl: backend !== Backend.Nextjs,
});
} else if (frontend === Frontend.ReactRouter) {
return rr7AutumnProvider({
includeBackendUrl: backend === Backend.Nextjs,
});
} else if (frontend === Frontend.Vite) {
return viteAutumnProvider();
} else {
return otherAutumnProvider();
}
};
export const AddAutumnProvider = () => {
const { queryStates } = useIntegrateContext();
return (
<div className="flex flex-col gap-4 w-full">
<StepHeader
number={4}
title={
<p>
Wrap your React app in <CodeSpan>{"<AutumnProvider />"}</CodeSpan>
</p>
}
/>
<p className="text-t2 text-sm">
This allows you to use our React hooks and components in your app. If
your server URL is different to your client, you will need to pass in
the backend URL as a prop.
</p>
<CodeBlock
snippets={[
{
title: "React App",
language: "javascript",
displayLanguage: "javascript",
content: getSnippet(queryStates),
},
]}
/>
{queryStates.auth === "supabase" && (
<InfoBox classNames={{ infoIcon: "!pt-1.5" }}>
<p className="text-t2 text-sm leading-5">
The examples above assume that Supabase auth is implemented using
server-side cookie authentication, following the guide{" "}
<a
className="text-t2 font-medium underline"
href="https://supabase.com/docs/guides/auth/server-side/creating-a-client"
target="_blank"
>
here
</a>
.
<br />
<br />
If you need to set <CodeSpan>{"Bearer <auth_token>"}</CodeSpan> in
your request headers to authenticate, you can use the{" "}
<CodeSpan>getBearerToken</CodeSpan> prop in the{" "}
<CodeSpan>AutumnProvider</CodeSpan> component.
</p>
</InfoBox>
)}
</div>
);
};

View File

@@ -12,22 +12,36 @@ import {
nextjsSupabaseOrg,
nextjsSupabaseUser,
} from "./snippets/nextjsHandler";
import { rr7BetterAuth, rr7Clerk, rr7Supabase } from "./snippets/rr7Handler";
import {
rr7BetterAuth,
rr7Clerk,
rr7Other,
rr7Supabase,
} from "./snippets/rr7Handler";
import {
honoBetterAuth,
honoClerk,
honoSupabase,
honoOther,
} from "./snippets/honoHandler";
import {
expressBetterAuth,
expressClerk,
expressOther,
expressSupabase,
} from "./snippets/expressHandler";
import { elysiaBetterAuth } from "./snippets/elysiaHandler";
import {
elysiaBetterAuth,
elysiaClerk,
elysiaOther,
} from "./snippets/elysiaHandler";
import { general } from "./snippets/general";
import { CodeSpan } from "../components/CodeSpan";
import { Backend } from "../StackEnums";
const snippet = () => {
return {
nextjs: {
[Backend.Nextjs]: {
["better_auth"]: {
user: nextjsBetterAuthUser,
org: nextjsBetterAuthOrg,
@@ -45,7 +59,7 @@ const snippet = () => {
org: nextjsOther,
},
},
["react_router"]: {
[Backend.ReactRouter]: {
["better_auth"]: {
user: rr7BetterAuth("user"),
org: rr7BetterAuth("org"),
@@ -58,8 +72,12 @@ const snippet = () => {
user: rr7Clerk("user"),
org: rr7Clerk("org"),
},
["other"]: {
user: rr7Other("user"),
org: rr7Other("org"),
},
},
["hono"]: {
[Backend.Hono]: {
["clerk"]: {
user: honoClerk("user"),
org: honoClerk("org"),
@@ -72,8 +90,12 @@ const snippet = () => {
user: honoBetterAuth("user"),
org: honoBetterAuth("org"),
},
["other"]: {
user: honoOther("user"),
org: honoOther("org"),
},
},
["express"]: {
[Backend.Express]: {
["better_auth"]: {
user: expressBetterAuth("user"),
org: expressBetterAuth("org"),
@@ -86,12 +108,24 @@ const snippet = () => {
user: expressSupabase("user"),
org: expressSupabase("org"),
},
["other"]: {
user: expressOther("user"),
org: expressOther("org"),
},
},
["elysia"]: {
[Backend.Elysia]: {
["better_auth"]: {
user: elysiaBetterAuth("user"),
org: elysiaBetterAuth("org"),
},
["clerk"]: {
user: elysiaClerk("user"),
org: elysiaClerk("org"),
},
["other"]: {
user: elysiaOther("user"),
org: elysiaOther("org"),
},
},
} as any;
};
@@ -106,21 +140,25 @@ export const AutumnHandler = () => {
const templates = snippet();
console.log("Backend Lang", backendLang);
console.log("Auth Provider", authProvider);
let template = templates[backendLang]?.[authProvider]?.[customerType] || "";
const template =
templates[backendLang]?.[authProvider]?.[customerType] || "";
template = template.trim("\n");
console.log("Template", template);
if (!template) {
return general();
}
// Trim \n from the template (only left and right)
return template.trim("\n");
return template;
};
return (
<div className="flex flex-col gap-2 w-full">
<StepHeader number={3} title="Install autumn-js" />
<div className="flex flex-col gap-4 w-full">
<StepHeader number={3} title="Mount Autumn handler" />
<p className="text-t2 text-sm">
<CodeSpan>autumnHandler</CodeSpan> mounts routes on the{" "}
<CodeSpan>/api/autumn/*</CodeSpan> paths which allows our React hooks
and components to interact with the Autumn API directly.
</p>
<CodeBlock
snippets={[

View File

@@ -0,0 +1,70 @@
import CodeBlock from "@/views/onboarding/components/CodeBlock";
import { CodeSpan } from "../components/CodeSpan";
import { StepHeader } from "../StepHeader";
import { Frontend } from "../StackEnums";
import { useIntegrateContext } from "../IntegrateContext";
import { InfoBox } from "../components/InfoBox";
const nextjsSnippet = () => {
return `import { PricingTable } from "autumn-js/react";
export default function Home() {
return (
<div className="h-screen w-screen flex justify-center items-center p-10">
<div className="w-full max-w-[800px]">
<PricingTable />
</div>
</div>
);
}`;
};
const getSnippet = (queryStates: any) => {
return nextjsSnippet();
};
export const CheckoutPricingTable = () => {
const { queryStates } = useIntegrateContext();
return (
<div className="flex flex-col gap-4 w-full">
<StepHeader
number={6}
title={
<p>
Drop in <CodeSpan>{"<PricingTable />"}</CodeSpan>
</p>
}
/>
<p className="text-t2 text-sm">
Display a pricing table with the plans you have created and let your
customers choose a plan.
</p>
<CodeBlock
snippets={[
{
title: "React App",
language: "javascript",
displayLanguage: "javascript",
content: getSnippet(queryStates),
},
]}
/>
<InfoBox classNames={{ infoIcon: "!pt-1.5" }}>
<p className="text-t2 text-sm leading-5">
{
"Our <PricingTable /> component is completely customisable by installing it as a shadcn component. "
}
Learn how to do so{" "}
<a
className="text-t2 font-medium underline"
href="https://docs.useautumn.com/setup/shadcn"
target="_blank"
>
here
</a>
.
</p>
</InfoBox>
</div>
);
};

View File

@@ -0,0 +1,114 @@
import { Input } from "@/components/ui/input";
import { DevContext } from "@/views/developer/DevContext";
import Step from "@/components/general/OnboardingStep";
import { useEnv } from "@/utils/envUtils";
import { useState } from "react";
import { toast } from "sonner";
import { DevService } from "@/services/DevService";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { Button } from "@/components/ui/button";
import { CheckIcon, CopyIcon, PlusIcon } from "lucide-react";
export const CreateSecretKey = ({
apiKey,
setApiKey,
}: {
apiKey: string;
setApiKey: (apiKey: string) => void;
}) => {
const env = useEnv();
// const [apiKeyName, setApiKeyName] = useState("");
const [apiCreated, setApiCreated] = useState(false);
const [loading, setLoading] = useState(false);
const [copied, setCopied] = useState(false);
const axiosInstance = useAxiosInstance({ env });
const handleCreate = async () => {
setLoading(true);
try {
const { api_key } = await DevService.createAPIKey(axiosInstance, {
name: "Autumn Onboarding",
});
setApiKey(api_key);
} catch (error) {
console.log("Error:", error);
toast.error("Failed to create API key");
}
setLoading(false);
};
return (
<DevContext.Provider
value={{
mutate: () => {},
onboarding: true,
apiCreated,
setApiCreated,
}}
>
<div className="flex flex-col gap-2 w-full">
{apiKey ? (
<div className="flex gap-2">
<Input value={apiKey} disabled className="w-[600px]" />
<Button
variant="secondary"
className="text-xs text-t3 flex gap-2 rounded-md shadow-none"
endIcon={
copied ? <CheckIcon size={12} /> : <CopyIcon size={12} />
}
onClick={() => {
navigator.clipboard.writeText(apiKey);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 1000);
}}
>
Copy
</Button>
</div>
) : (
<div className="flex gap-2">
{/* <Input
placeholder="Secret API Key Name"
className="w-full"
value={apiKeyName}
disabled={apiCreated}
onChange={(e) => setApiKeyName(e.target.value)}
/> */}
<Button
onClick={handleCreate}
isLoading={loading}
variant="outline"
startIcon={<PlusIcon size={14} />}
>
Create Secret Key
</Button>
</div>
)}
</div>
</DevContext.Provider>
);
};
{
/* <div className="border rounded-sm px-2 py-1">
{env === AppEnv.Sandbox ? (
<CopyPublishableKey
type="Sandbox"
value={productData?.org?.test_pkey}
/>
) : (
<CopyPublishableKey
type="Production"
value={productData?.org?.live_pkey}
/>
)}
</div> */
}

View File

@@ -0,0 +1,36 @@
import CodeBlock from "@/views/onboarding/components/CodeBlock";
import { CodeSpan } from "../components/CodeSpan";
import { StepHeader } from "../StepHeader";
import { useState } from "react";
import { CreateSecretKey } from "./CreateSecretKey";
export const EnvStep = () => {
const [apiKey, setApiKey] = useState("");
return (
<>
<div className="flex flex-col gap-4 w-full">
<StepHeader
number={2}
title={
<p>
Add the Autumn secret key to your <CodeSpan>{".env"}</CodeSpan>{" "}
file
</p>
}
/>
<CreateSecretKey apiKey={apiKey} setApiKey={setApiKey} />
<CodeBlock
snippets={[
{
title: ".env",
language: "bash",
displayLanguage: "bash",
content: `AUTUMN_SECRET_KEY=${apiKey || "am_sk_12345"}`,
},
]}
/>
</div>
</>
);
};

View File

@@ -4,7 +4,7 @@ export const elysiaBetterAuth = (customerType: "user" | "org") => {
return `import { autumnHandler } from "autumn-js/elysia";
import { auth } from "./auth";
const app = new Elysia({ adapter: node() })
const app = new Elysia()
.use(cors())
.mount(auth.handler)
.use(
@@ -19,8 +19,60 @@ ${betterAuthSnippet(customerType, "context.headers", 4)}
`;
};
// export const elysiaOther = (customerType: "user" | "org") => {
// return `
export const elysiaClerk = (customerType: "user" | "org") => {
return `import { clerkPlugin } from "elysia-clerk";
import { autumnHandler } from "autumn-js/backend";
// `;
// };
const app = new Elysia()
.use(cors())
.use(clerkPlugin())
.all("*", async (ctx: any) => {
console.log("Request received");
})
.all("/api/autumn/*", async (ctx: any) => {
const auth = ctx.auth();
let body = null;
if (ctx.request.method !== "GET") {
body = await ctx.request.json();
}
const { statusCode, response } = await autumnHandler({
customerId: ${customerType === "user" ? "auth.userId" : "auth.orgId"},
customerData: { name: "", email: "" },
request: {
url: ctx.request.url,
method: ctx.request.method,
body: body,
},
});
ctx.set.status = statusCode;
return response;
})
.listen(8000);`;
};
export const elysiaOther = (customerType: "user" | "org") => {
return `import { autumnHandler } from "autumn-js/elysia";
import { auth } from "./auth";
const app = new Elysia()
.use(cors())
.mount(auth.handler)
.use(
autumnHandler({
identify: async (context) => {
const customerId = "your_customer_id"; // Authenticate and get customer ID from your DB
return {
customerId,
customerData: { name: "", email: "" },
};
},
})
)
.listen(8000);
`;
};

View File

@@ -46,3 +46,22 @@ app.use(
})
);`;
};
export const expressOther = (customerType: "user" | "org") => {
return `import { autumnHandler } from "autumn-js/express";
app.use(express.json()); // need to parse request body before autumnHandler
app.use(
"/api/autumn",
autumnHandler({
identify: async (req, res) => {
const customerId = "your_customer_id"; // Get customer id from your database
return {
customerId,
customerData: { name: "", email: "" },
};
},
})
);`;
};

View File

@@ -0,0 +1,41 @@
export const general = () => {
return `import { autumnHandler } from "autumn-js/backend";
// 1. autumnHandler takes in request properties and returns a response
// 2. Simply mount the handler onto the /api/autumn/* path in your backend
// 3. Call autumnHandler and pass in the required parameters
// 4. Return the response from the autumnHandler
// Example using autumnHandler with Hono & Clerk
import { autumnHandler } from "autumn-js/backend";
import { clerkMiddleware, getAuth } from "@hono/clerk-auth";
app.use("*", clerkMiddleware());
app.use(
"/api/autumn/*",
async (c) => {
const auth = getAuth(c);
if (!auth?.userId) {
return c.json({ message: "Unauthorized" }, 401);
}
let body = null;
if (c.req.method !== "GET") {
body = await c.req.json();
}
const { statusCode, response } = await autumnHandler({
customerId: auth.userId,
customerData: { name: "", email: "" },
request: {
url: c.req.url,
method: c.req.method,
body: body,
},
});
return c.json(response, statusCode);
}
);`;
};

View File

@@ -104,3 +104,23 @@ ${betterAuthSnippet(customerType, "c.req.raw.headers", 3)}
})
);`;
};
export const honoOther = (customerType: "user" | "org") => {
return `// index.ts
import { autumnHandler } from "autumn-js/hono";
import { auth } from "@/lib/auth"
app.use(
"/api/autumn/*",
autumnHandler({
identify: async (c: Context) => {
const customerId = "your_customer_id"; // Get customer id from your database
return {
customerId,
customerData: { name: "", email: "" },
};
},
})
);`;
};

View File

@@ -50,8 +50,8 @@ export const { GET, POST } = autumnHandler({
export const nextjsSupabaseUser = `
// app/api/autumn/[...all]/route.ts
import { createClient } from "@/utils/supabase/server";
import { autumnHandler } from "autumn-js/next";
import { createClient } from "@/utils/supabase/server";
export const { GET, POST } = autumnHandler({
identify: async () => {
@@ -75,8 +75,8 @@ export const { GET, POST } = autumnHandler({
export const nextjsSupabaseOrg = `
// app/api/autumn/[...all]/route.ts
import { createClient } from "@/utils/supabase/server";
import { autumnHandler } from "autumn-js/next";
import { createClient } from "@/utils/supabase/server";
export const { GET, POST } = autumnHandler({
identify: async () => {
@@ -150,13 +150,10 @@ import { autumnHandler } from "autumn-js/next";
export const { GET, POST } = autumnHandler({
identify: async (request) => {
// Authenticate the request and get the customer ID
const customerId = "customer_id";
// Authenticate the request and get the customer ID
const customerId = "customer_id";
return {
customerId,
// To store the customer name and email
customerData: { name: "", email: "" },
};
},

View File

@@ -71,3 +71,23 @@ const handler = autumnHandler({
export const loader = handler.loader;
export const action = handler.action;`;
};
export const rr7Other = (customerType: "user" | "org") => {
return `// app/routes/api.autumn.tsx
import { autumnHandler } from "autumn-js/react-router";
const handler = autumnHandler({
secretKey: process.env.AUTUMN_SECRET_KEY!,
identify: async (args) => {
const customerId = "your_customer_id"; // Get customer id from your database
return {
customerId,
customerData: { name: "", email: "" },
};
},
});
export const loader = handler.loader;
export const action = handler.action;`;
};

View File

@@ -0,0 +1,172 @@
import { Code, Fingerprint, CircleUserRound, Building } from "lucide-react";
import { useIntegrateContext } from "../IntegrateContext";
import { Backend, Frontend } from "../StackEnums";
export const SelectFrameworks = () => {
const { queryStates, setQueryStates } = useIntegrateContext();
const iconSize = 12;
const frontendOptions = [
{ value: Frontend.Nextjs, label: "Next.js", logo: "nextjs.png" },
{ value: Frontend.ReactRouter, label: "RR7", logo: "react-router.svg" },
{ value: Frontend.Vite, label: "Vite SPA", logo: "vite.svg" },
{
value: Frontend.Other,
label: "Other",
icon: <Code size={iconSize} className="text-t2" />,
},
];
const backendOptions = [
{ value: Backend.Nextjs, label: "Next.js", logo: "nextjs.png" },
{
value: Backend.ReactRouter,
label: "RR7",
logo: "react-router.svg",
},
{ value: Backend.Hono, label: "Hono", logo: "hono.png" },
{ value: Backend.Express, label: "Express", logo: "express.png" },
{ value: Backend.Elysia, label: "Elysia", logo: "elysia.png" },
{
value: Backend.Other,
label: "Other",
icon: <Code size={iconSize} className="text-t2" />,
},
];
const authOptions = [
{ value: "better_auth", label: "Better Auth", logo: "better-auth.png" },
{ value: "supabase", label: "Supabase", logo: "supabase.png" },
{ value: "clerk", label: "Clerk", logo: "clerk.png" },
{
value: "other",
label: "Other",
icon: <Fingerprint size={iconSize} className="text-t2" />,
},
];
const customerOptions = [
{
value: "user",
label: "Users",
icon: <CircleUserRound size={iconSize} className="text-t2" />,
},
{
value: "org",
label: "Orgs",
icon: <Building size={iconSize} className="text-t2" />,
},
];
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-t2">Frontend</label>
<div className="flex flex-row gap-2">
{frontendOptions.map((option) => {
return (
<FrameworkContainer
option={option}
setQueryStates={setQueryStates}
queryStates={queryStates}
type="frontend"
/>
);
})}
</div>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-t2">Backend</label>
<div className="flex flex-row gap-2">
{backendOptions.map((option) => {
return (
<FrameworkContainer
option={option}
setQueryStates={setQueryStates}
queryStates={queryStates}
type="backend"
/>
);
})}
</div>
</div>
<div className="flex w-full justify-between">
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-t2">Auth Provider</label>
<div className="flex flex-row gap-2">
{authOptions.map((option) => {
return (
<FrameworkContainer
option={option}
setQueryStates={setQueryStates}
queryStates={queryStates}
type="auth"
/>
);
})}
</div>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-t2">Customer Type</label>
<div className="flex flex-row gap-2">
{customerOptions.map((option) => {
return (
<FrameworkContainer
option={option}
setQueryStates={setQueryStates}
queryStates={queryStates}
type="customerType"
/>
);
})}
</div>
</div>
</div>
</div>
);
};
const FrameworkContainer = ({
option,
setQueryStates,
queryStates,
type,
}: {
option: {
value: string;
label: string;
logo?: string;
icon?: React.ReactNode;
};
setQueryStates: (queryStates: any) => void;
queryStates: any;
type: "backend" | "auth" | "customerType" | "frontend";
}) => {
const isSelected = queryStates[type] === option.value;
return (
<div
key={option.value}
className={`flex text-sm justify-center items-center gap-2 h-8 px-2 rounded-xs cursor-pointer ${
isSelected
? "shadow-inner bg-stone-100 border border-zinc-200"
: "border border-transparent"
}`}
onClick={() => setQueryStates({ ...queryStates, [type]: option.value })}
>
{option.logo ? (
<img
src={`/frameworks/${option.logo}`}
alt={option.label}
className="w-3 h-3"
/>
) : option.icon ? (
<>{option.icon}</>
) : (
<Code className="text-t2" />
)}
<span className="text-t2 text-sm font-medium whitespace-nowrap">
{option.label}
</span>
</div>
);
};