Redo setup page onboarding UI (#1659)

## Summary
- Reworks the setup page into recommended/manual onboarding flows with
clearer setup guidance and an explicit MCP verification step.
- Polishes setup UI surfaces, framework selector cards, step indicators,
code blocks, and key display styling.
- Replaces raw env key textareas with a masked env-file viewer and
reveal/copy controls.

## Screenshots
### Recommended Setup
| | Light | Dark |
| --- | --- | --- |
| Overview | ![Recommended setup light
overview](https://gist.githubusercontent.com/Developing-Gamer/71107f429eeb4f17a1aed5394f3ec220/raw/01-recommended-light-top.png)
| ![Recommended setup dark
overview](https://gist.githubusercontent.com/Developing-Gamer/71107f429eeb4f17a1aed5394f3ec220/raw/03-recommended-dark-top.png)
|
| Keys | ![Recommended setup light
keys](https://gist.githubusercontent.com/Developing-Gamer/71107f429eeb4f17a1aed5394f3ec220/raw/02-recommended-light-keys.png)
| ![Recommended setup dark
keys](https://gist.githubusercontent.com/Developing-Gamer/71107f429eeb4f17a1aed5394f3ec220/raw/04-recommended-dark-keys.png)
|

### Manual Setup
Framework selector:

![Manual setup framework
selector](https://gist.githubusercontent.com/Developing-Gamer/71107f429eeb4f17a1aed5394f3ec220/raw/05-manual-dark-frameworks.png)

Next.js keys:

![Manual setup Next.js
keys](https://gist.githubusercontent.com/Developing-Gamer/71107f429eeb4f17a1aed5394f3ec220/raw/06-manual-dark-next-keys.png)

Code block syntax highlighting:

![Manual setup code
blocks](https://gist.githubusercontent.com/Developing-Gamer/71107f429eeb4f17a1aed5394f3ec220/raw/07-manual-dark-codeblocks.png)

TanStack Start keys:

![Manual setup TanStack
keys](https://gist.githubusercontent.com/Developing-Gamer/71107f429eeb4f17a1aed5394f3ec220/raw/08-manual-dark-tanstack-keys.png)

## Test plan
- [x] Ran `pnpm --filter @hexclave/dashboard lint`
- [x] Manually checked recommended setup, manual setup, masked/revealed
keys, and light/dark mode screenshots

Made with [Cursor](https://cursor.com)

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Redesigned the Setup page around a single cloud‑first prompt and a
docs‑linked Manual path, with a masked `.env` viewer and refreshed code
panels. Environment‑aware docs/API URLs, explicit MCP verification, and
stricter tab handling make onboarding clearer and safer.

- **New Features**
- Recommended flow shows a prefilled cloud setup prompt (API URL and
project ID) inline; Manual opens the latest setup docs.
- Replaced textareas with `EnvFileViewer` for `.env.local`/`.env`
(reveal-all and copy file); “Generate Keys” CTA when keys are missing.
- Secret inputs in `CopyField` support show/hide; `CodeBlock` uses
shared `codePanelShellClasses` and header.
  - Added a final “Done” step with an Explore Dashboard CTA.

- **Bug Fixes**
- MCP server registration details are correct and explicitly called out
for verification (name `hexclave`, URL `https://mcp.hexclave.com/mcp`).
- Tabs fail fast on unexpected values; docs/API base URLs read from
`NEXT_PUBLIC_STACK_DOCS_BASE_URL`/`NEXT_PUBLIC_STACK_API_URL` with
fallbacks; updated `.env.development` docs URL.

<sup>Written for commit 2cd3486ac6.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1659?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Setup onboarding now uses “Recommended” and “Manual setup” tabs with
an ordered checklist and a shared in-page setup prompt.
* Environment key generation now uses a line-by-line env file viewer
with masking/reveal and copy support, including “Generate Keys” when
keys are missing.
  * Secret fields now include show/hide toggles.
* **Bug Fixes**
* Documentation links now open the correct constructed URLs based on the
configured docs base URL (with a sensible fallback).
* **Style**
* Refreshed code panel and input/textarea theming, including improved
light/dark styling and updated copy-field behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: armaan <armaan@stack-auth.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Armaan Jain 2026-06-26 11:13:56 -07:00 committed by GitHub
parent d2a84f5a28
commit 2474b600de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 284 additions and 699 deletions

View File

@ -1,5 +1,5 @@
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}26
NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}04
NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09
NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR=false

View File

@ -1,17 +1,17 @@
'use client';
import { CodeBlock } from '@/components/code-block';
import { CodeBlock, codePanelShellClasses } from '@/components/code-block';
import { DesignButton } from "@/components/design-components";
import { APIEnvKeys, NextJsEnvKeys, ViteEnvKeys } from '@/components/env-keys';
import { EnvFileViewer } from '@/components/env-keys';
import { InlineCode } from '@/components/inline-code';
import { StyledLink } from '@/components/link';
import { CopyPromptButton, Tabs, TabsContent, TabsList, TabsTrigger, Typography, cn } from "@/components/ui";
import { Tabs, TabsList, TabsTrigger, Typography, cn } from "@/components/ui";
import { getPublicEnvVar } from '@/lib/env';
import { useThemeWatcher } from '@/lib/theme';
import { BookIcon, SparkleIcon, XIcon } from "@phosphor-icons/react";
import { BookIcon, XIcon } from "@phosphor-icons/react";
import { remindersPrompt } from '@hexclave/shared/dist/ai/unified-prompts/reminders';
import { use } from "@hexclave/shared/dist/utils/react";
import { deindent } from '@hexclave/shared/dist/utils/strings';
import dynamic from "next/dynamic";
import Image from 'next/image';
import { Suspense, useRef, useState } from "react";
import type { GlobeMethods } from 'react-globe.gl';
import { PageLayout } from "../page-layout";
@ -22,71 +22,63 @@ import styles from './setup-page.module.css';
const countriesPromise = import('./country-data.geo.json');
const Globe = dynamic(() => import('react-globe.gl').then((mod) => mod.default), { ssr: false });
const commandClasses = "text-red-600 dark:text-red-400";
const nameClasses = "text-green-600 dark:text-green-500";
type SetupMode = "recommended" | "manual";
const INSTALL_COMMAND_BY_FRAMEWORK = {
nextjs: 'npx @hexclave/cli@latest init',
tanstackStart: 'npm install @hexclave/tanstack-start',
react: 'npm install @hexclave/react',
javascript: 'npm install @hexclave/js',
python: 'pip install requests',
} as const;
const PROD_DOCS_BASE_URL = 'https://docs.hexclave.com';
const PROD_API_BASE_URL = 'https://api.hexclave.com';
type SetupFramework = keyof typeof INSTALL_COMMAND_BY_FRAMEWORK;
function getSetupDocsBaseUrl() {
return getPublicEnvVar('NEXT_PUBLIC_STACK_DOCS_BASE_URL') ?? PROD_DOCS_BASE_URL;
}
const TANSTACK_START_SETUP_PROMPT = deindent`
Please set up Hexclave in my TanStack Start app.
function getManualSetupDocsUrl() {
const docsBaseUrl = getSetupDocsBaseUrl().replace(/\/$/, '');
return `${docsBaseUrl}/guides/getting-started/setup`;
}
1. Install the alpha TanStack Start package:
function getSetupApiBaseUrl() {
return getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL') ?? PROD_API_BASE_URL;
}
npm install @hexclave/tanstack-start
function buildCloudSetupPrompt(options: {
docsBaseUrl: string,
projectId: string,
apiBaseUrl: string,
}) {
const { docsBaseUrl, projectId, apiBaseUrl } = options;
const normalizedDocsBaseUrl = docsBaseUrl.replace(/\/$/, '');
const reminders = remindersPrompt.replaceAll(PROD_DOCS_BASE_URL, normalizedDocsBaseUrl);
2. Configure the app with these environment variables:
VITE_HEXCLAVE_PROJECT_ID=<project-id>
HEXCLAVE_SECRET_SERVER_KEY=<secret-server-key>
3. Create a HexclaveClientApp using @hexclave/tanstack-start with:
- projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID
- tokenStore: "cookie"
- redirectMethod: "window"
4. Wrap the TanStack Start root route with HexclaveProvider and HexclaveTheme.
5. Add a /handler/$ route using HexclaveHandler. The handler route must set ssr: false and pass location={pathname} from useLocation().
Use only the environment variables listed above.
After it finishes, verify that the Hexclave MCP server is registered in your AI client config name: \`hexclave\`, transport: \`http\`, URL: \`https://mcp.hexclave.com/\`. If it is not registered, please add it manually so you have live access to Hexclave docs and APIs.
`;
const buildInstallPrompt = (framework: SetupFramework) => {
if (framework === "tanstackStart") {
return TANSTACK_START_SETUP_PROMPT;
}
const command = INSTALL_COMMAND_BY_FRAMEWORK[framework];
return deindent`
Please run the following command in my project's terminal:
Install and set up Hexclave in this project by following these instructions:
${command}
Read https://skill.hexclave.com and follow the setup instructions it gives for this project's specific framework and language.
After it finishes, verify that the Hexclave MCP server is registered in your AI client config name: \`stack-auth\`, transport: \`http\`, URL: \`https://mcp.hexclave.com/mcp\`. The command above should handle this automatically; if for any reason it didn't, please add the MCP server manually so you have live access to Hexclave docs and APIs.
`;
};
Follow skill.hexclave.com as written, but make sure to use the cloud setup, not the local dashboard setup.
Do not change the dev script in package.json. In cloud setup, there's no need for that.
Use these Hexclave project values when creating environment variables:
- Hexclave API URL: ${apiBaseUrl}
- Hexclave project ID: ${projectId}
Create the framework-specific public environment variables for the Hexclave API URL and project ID. For example, Next.js uses NEXT_PUBLIC_HEXCLAVE_API_URL and NEXT_PUBLIC_HEXCLAVE_PROJECT_ID, while Vite-based frameworks use VITE_HEXCLAVE_API_URL and VITE_HEXCLAVE_PROJECT_ID. If the Hexclave docs for this framework specify different environment variable names, use the docs' framework-specific names with the values above.
After setup finishes, verify that the Hexclave MCP server is registered in your AI client config name: \`hexclave\`, transport: \`http\`, URL: \`https://mcp.hexclave.com/mcp\`. If it is not registered, add it manually so future agents have live access to Hexclave docs and APIs.
Once setup is done, tell me to add the Hexclave secret server key from the dashboard to my environment file. After that, setup is complete.
${reminders}
`;
}
export default function SetupPage(props: { toMetrics: () => void }) {
const adminApp = useAdminApp();
const [selectedFramework, setSelectedFramework] = useState<'nextjs' | 'tanstackStart' | 'react' | 'javascript' | 'python'>('nextjs');
const [setupMode, setSetupMode] = useState<SetupMode>("recommended");
const [keys, setKeys] = useState<{ projectId: string, publishableClientKey?: string, secretServerKey: string } | null>(null);
const projectConfig = adminApp.useProject().useConfig();
const requirePublishableClientKey = projectConfig.project.requirePublishableClientKey;
const publishableClientKeyValue = keys?.publishableClientKey ?? "...";
const optionalPublishableClientKeyProp = (indent: string) =>
requirePublishableClientKey ? `\n${indent}publishableClientKey: "${publishableClientKeyValue}",` : "";
const optionalPublishableClientKeyHeader = (indent: string) =>
requirePublishableClientKey ? `\n${indent}'x-hexclave-publishable-client-key': "${publishableClientKeyValue}",` : "";
const onGenerateKeys = async () => {
const newKey = await adminApp.createInternalApiKey({
@ -104,512 +96,12 @@ export default function SetupPage(props: { toMetrics: () => void }) {
});
};
const nextJsSteps = [
{
step: 2,
title: "Install Hexclave",
content: <>
<Typography>
In a new or existing Next.js project, install Hexclave as a dependency into your project:
</Typography>
<CodeBlock
language="bash"
content={`npx @hexclave/cli@latest init`}
customRender={
<div className="p-4 font-mono text-sm">
<span className={commandClasses}>pnpx</span> <span className={nameClasses}>@hexclave/cli@latest</span> init
</div>
}
title="Terminal"
icon="terminal"
/>
</>
},
{
step: 3,
title: "Create Keys",
content: <>
<Typography>
Put these keys in the <InlineCode>.env.local</InlineCode> file.
</Typography>
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="next" />
</>
},
{
step: 4,
title: "Done",
content: <>
<Typography>
If you start your Next.js app with npm run dev and navigate to <StyledLink href="http://localhost:3000/handler/signup">http://localhost:3000/handler/signup</StyledLink>, you will see the sign-up page.
</Typography>
</>
},
];
const reactSteps = [
{
step: 2,
title: "Install Hexclave",
content: <>
<Typography>
In a new or existing React project, install Hexclave&apos;s dependencies:
</Typography>
<CodeBlock
language="bash"
content={`npm install @hexclave/react`}
customRender={
<div className="p-4 font-mono text-sm">
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@hexclave/react</span>
</div>
}
title="Terminal"
icon="terminal"
/>
</>
},
{
step: 3,
title: "Create Keys",
content: <HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="raw" />
},
{
step: 4,
title: "Create hexclave/client.ts file",
content: <>
<Typography>
Create a new file called <InlineCode>hexclave/client.ts</InlineCode> and add the following code. Here we use react-router-dom as an example.
</Typography>
<CodeBlock
language="tsx"
content={deindent`
import { HexclaveClientApp } from "@hexclave/react";
import { useNavigate } from "react-router-dom";
export const hexclaveClientApp = new HexclaveClientApp({
// You should store these in environment variables
projectId: "${keys?.projectId ?? "..."}",${optionalPublishableClientKeyProp(" ")}
tokenStore: "cookie",
redirectMethod: {
useNavigate,
}
});
`}
title="hexclave/client.ts"
icon="code"
/>
</>
},
{
step: 5,
title: "Update App.tsx",
content: <>
<Typography>
Update your App.tsx file to wrap the entire app with a <InlineCode>HexclaveProvider</InlineCode> and <InlineCode>HexclaveTheme</InlineCode> and add a <InlineCode>HexclaveHandler</InlineCode> component to handle the authentication flow.
</Typography>
<CodeBlock
language="tsx"
maxHeight={300}
content={deindent`
import { HexclaveHandler, HexclaveProvider, HexclaveTheme } from "@hexclave/react";
import { Suspense } from "react";
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom";
import { hexclaveClientApp } from "./hexclave/client";
function HandlerRoutes() {
const location = useLocation();
return (
<HexclaveHandler app={hexclaveClientApp} location={location.pathname} fullPage />
);
}
export default function App() {
return (
<Suspense fallback={"Loading..."}>
<BrowserRouter>
<HexclaveProvider app={hexclaveClientApp}>
<HexclaveTheme>
<Routes>
<Route path="/handler/*" element={<HandlerRoutes />} />
<Route path="/" element={<div>hello world</div>} />
</Routes>
</HexclaveTheme>
</HexclaveProvider>
</BrowserRouter>
</Suspense>
);
}
`}
title="App.tsx"
icon="code"
/>
</>
},
{
step: 6,
title: "Done",
content: <>
<Typography>
If you start your React app with npm run dev and navigate to <StyledLink href="http://localhost:5173/handler/signup">http://localhost:5173/handler/signup</StyledLink>, you will see the sign-up page.
</Typography>
</>
}
];
const tanstackStartSteps = [
{
step: 2,
title: "Install Hexclave",
content: <>
<Typography>
In a new or existing TanStack Start project, install the alpha Hexclave package:
</Typography>
<CodeBlock
language="bash"
content={`npm install @hexclave/tanstack-start`}
customRender={
<div className="p-4 font-mono text-sm">
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@hexclave/tanstack-start</span>
</div>
}
title="Terminal"
icon="terminal"
/>
</>
},
{
step: 3,
title: "Create Keys",
content: <>
<Typography>
Put these keys in your TanStack Start environment file.
</Typography>
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="vite" />
</>
},
{
step: 4,
title: "Create hexclave/client.ts file",
content: <>
<Typography>
Create a new file called <InlineCode>src/hexclave/client.ts</InlineCode> and initialize Hexclave with cookie storage.
</Typography>
<CodeBlock
language="tsx"
content={deindent`
import { HexclaveClientApp } from "@hexclave/tanstack-start";
export const hexclaveClientApp = new HexclaveClientApp({
projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID,
tokenStore: "cookie",
redirectMethod: "window",
});
`}
title="src/hexclave/client.ts"
icon="code"
/>
</>
},
{
step: 5,
title: "Update the root route",
content: <>
<Typography>
Wrap your TanStack Start root route with <InlineCode>HexclaveProvider</InlineCode> and <InlineCode>HexclaveTheme</InlineCode>.
</Typography>
<CodeBlock
language="tsx"
maxHeight={300}
content={deindent`
import { HexclaveProvider, HexclaveTheme } from "@hexclave/tanstack-start";
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
import { hexclaveClientApp } from "../hexclave/client";
export const Route = createRootRoute({
component: RootComponent,
shellComponent: RootDocument,
});
function RootComponent() {
return (
<HexclaveProvider app={hexclaveClientApp}>
<HexclaveTheme>
<Outlet />
</HexclaveTheme>
</HexclaveProvider>
);
}
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
);
}
`}
title="src/routes/__root.tsx"
icon="code"
/>
</>
},
{
step: 6,
title: "Add the handler route",
content: <>
<Typography>
Create a splat route for Hexclave&apos;s built-in auth pages.
</Typography>
<CodeBlock
language="tsx"
content={deindent`
import { HexclaveHandler } from "@hexclave/tanstack-start";
import { createFileRoute, useLocation } from "@tanstack/react-router";
export const Route = createFileRoute("/handler/$")({
ssr: false,
component: HandlerPage,
});
function HandlerPage() {
const { pathname } = useLocation();
return <HexclaveHandler fullPage location={pathname} />;
}
`}
title="src/routes/handler/$.tsx"
icon="code"
/>
<Typography>
If you start your TanStack Start app and navigate to <StyledLink href="http://localhost:3000/handler/sign-up">http://localhost:3000/handler/sign-up</StyledLink>, you will see the sign-up page.
</Typography>
</>
},
];
const javascriptSteps = [
{
step: 2,
title: "Install Hexclave",
content: <>
<Typography>
Install Hexclave using npm:
</Typography>
<CodeBlock
language="bash"
content={`npm install @hexclave/js`}
customRender={
<div className="p-4 font-mono text-sm">
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@hexclave/js</span>
</div>
}
title="Terminal"
icon="terminal"
/>
</>
},
{
step: 3,
title: "Create Keys",
content: <HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="raw" />
},
{
step: 4,
title: "Initialize the app",
content: <>
<Typography>
Create a new file for your Hexclave app initialization:
</Typography>
<Tabs defaultValue="server">
<TabsList>
<TabsTrigger value="server">Server</TabsTrigger>
<TabsTrigger value="client">Client</TabsTrigger>
</TabsList>
<TabsContent value="server">
<CodeBlock
language="typescript"
content={deindent`
import { HexclaveServerApp } from "@hexclave/js";
const hexclaveServerApp = new HexclaveServerApp({
// You should store these in environment variables based on your project setup
projectId: "${keys?.projectId ?? "..."}",${optionalPublishableClientKeyProp(" ")}
secretServerKey: "${keys?.secretServerKey ?? "..."}",
tokenStore: "memory",
});
`}
title="hexclave/server.ts"
icon="code"
/>
</TabsContent>
<TabsContent value="client">
<CodeBlock
language="typescript"
content={deindent`
import { HexclaveClientApp } from "@hexclave/js";
const hexclaveClientApp = new HexclaveClientApp({
// You should store these in environment variables
projectId: "your-project-id",${optionalPublishableClientKeyProp(" ")}
tokenStore: "cookie",
});
`}
title="hexclave/client.ts"
icon="code"
/>
</TabsContent>
</Tabs>
</>
},
{
step: 5,
title: "Example usage",
content: <>
<Tabs defaultValue="server">
<TabsList>
<TabsTrigger value="server">Server</TabsTrigger>
<TabsTrigger value="client">Client</TabsTrigger>
</TabsList>
<TabsContent value="server">
<CodeBlock
language="typescript"
content={deindent`
import { hexclaveServerApp } from "@/hexclave/server";
const user = await hexclaveServerApp.getUser("user_id");
await user.update({
displayName: "New Display Name",
});
const team = await hexclaveServerApp.createTeam({
name: "New Team",
});
await team.addUser(user.id);
`}
title="Example server usage"
icon="code"
/>
</TabsContent>
<TabsContent value="client">
<CodeBlock
language="typescript"
content={deindent`
import { hexclaveClientApp } from "@/hexclave/client";
await hexclaveClientApp.signInWithCredential({
email: "test@example.com",
password: "password123",
});
const user = await hexclaveClientApp.getUser();
await user.update({
displayName: "New Display Name",
});
await user.signOut();
`}
title="Example client usage"
icon="code"
/>
</TabsContent>
</Tabs>
</>
}
];
const pythonSteps = [
{
step: 2,
title: "Install requests",
content: <>
<Typography>
Install the requests library to make HTTP requests to the Hexclave API:
</Typography>
<CodeBlock
language="bash"
content={`pip install requests`}
customRender={
<div className="p-4 font-mono text-sm">
<span className={commandClasses}>pip install</span> <span className={nameClasses}>requests</span>
</div>
}
title="Terminal"
icon="terminal"
/>
</>
},
{
step: 3,
title: "Create Keys",
content: <HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="raw" />
},
{
step: 4,
title: "Create helper function",
content: <>
<Typography>
Create a helper function to make requests to the Hexclave API:
</Typography>
<CodeBlock
language="python"
content={deindent`
import requests
def stack_auth_request(method, endpoint, **kwargs):
res = requests.request(
method,
f'https://api.hexclave.com/{endpoint}',
headers={
'x-hexclave-access-type': 'server',
# You should store these in environment variables
'x-hexclave-project-id': "${keys?.projectId ?? "..."}",${optionalPublishableClientKeyHeader(" ")}
'x-hexclave-secret-server-key': "${keys?.secretServerKey ?? "..."}",
**kwargs.pop('headers', {}),
},
**kwargs,
)
if res.status_code >= 400:
raise Exception(f"Hexclave API request failed with {res.status_code}: {res.text}")
return res.json()
`}
title="stack_auth.py"
icon="code"
/>
</>
},
{
step: 5,
title: "Make requests",
content: <>
<Typography>
You can now make requests to the Hexclave API:
</Typography>
<CodeBlock
language="python"
content={deindent`
# Get current project info
print(stack_auth_request('GET', '/api/v1/projects/current'))
# Get user info with access token
print(stack_auth_request('GET', '/api/v1/users/me', headers={
'x-hexclave-access-token': access_token,
}))
`}
title="example.py"
icon="code"
/>
</>
}
];
const selectedInstallPrompt = buildCloudSetupPrompt({
docsBaseUrl: getSetupDocsBaseUrl(),
projectId: adminApp.projectId,
apiBaseUrl: getSetupApiBaseUrl(),
});
const manualSetupDocsUrl = getManualSetupDocsUrl();
return (
<PageLayout width={1000}>
@ -619,7 +111,7 @@ export default function SetupPage(props: { toMetrics: () => void }) {
<XIcon className="w-4 h-4 ml-1 mt-0.5" />
</DesignButton>
</div>
<div className="flex gap-4 justify-center items-center border rounded-2xl py-4 px-8 backdrop-blur-md bg-slate-200/20 dark:bg-black/20">
<div className="flex gap-4 justify-center items-center rounded-2xl py-4 px-8 backdrop-blur-md bg-white/60 dark:bg-background/40 ring-1 ring-black/[0.06] dark:ring-white/[0.06] border border-black/[0.06] dark:border-white/[0.06] shadow-sm">
<GlobeIllustration />
<div className="flex flex-col gap-4">
@ -638,7 +130,7 @@ export default function SetupPage(props: { toMetrics: () => void }) {
variant='outline'
size='sm'
onClick={() => {
window.open('https://docs.hexclave.com/', '_blank');
window.open(getSetupDocsBaseUrl(), '_blank');
}}
>
<BookIcon className="w-4 h-4 mr-2" />
@ -649,91 +141,81 @@ export default function SetupPage(props: { toMetrics: () => void }) {
</div>
<div className="flex justify-end mt-8 mx-4">
<CopyPromptButton
variant="outline"
size="sm"
content={buildInstallPrompt(selectedFramework)}
>
<SparkleIcon className="w-4 h-4 mr-2 text-purple-500 dark:text-purple-400" weight="fill" />
Copy prompt
</CopyPromptButton>
<Tabs value={setupMode} onValueChange={(value) => setSetupMode(value === "manual" ? "manual" : "recommended")}>
<TabsList>
<TabsTrigger value="recommended">Recommended</TabsTrigger>
<TabsTrigger value="manual">Manual setup</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="flex flex-col mt-4 mx-4">
<ol className="relative text-gray-500 border-s border-gray-200 dark:border-gray-700 dark:text-gray-400 ">
{[
{
step: 1,
title: "Select your framework",
content: <div>
<div className="flex gap-4 flex-wrap">
{([{
id: 'nextjs',
name: 'Next.js',
reverseIfDark: true,
imgSrc: '/next-logo.svg',
}, {
id: 'tanstackStart',
name: 'TanStack Start',
reverseIfDark: false,
imgSrc: '/tanstack-start-logo.png',
}, {
id: 'react',
name: 'React',
reverseIfDark: false,
imgSrc: '/react-logo.svg',
}, {
id: 'javascript',
name: 'JavaScript',
reverseIfDark: false,
imgSrc: '/javascript-logo.svg',
}, {
id: 'python',
name: 'Python',
reverseIfDark: false,
imgSrc: '/python-logo.svg',
}] as const).map(({ name, imgSrc: src, reverseIfDark, id }) => (
<DesignButton
key={id}
variant={id === selectedFramework ? 'secondary' : 'plain'} className='h-24 w-24 flex flex-col items-center justify-center gap-2 '
onClick={() => setSelectedFramework(id)}
>
<Image
src={src}
alt={name}
className={reverseIfDark ? "dark:invert" : undefined}
width="0"
height="0"
sizes="100vw"
style={{ width: '30px', height: 'auto' }}
/>
<Typography type='label'>{name}</Typography>
</DesignButton>
))}
{setupMode === "recommended" ? (
<div className="flex flex-col mt-4 mx-4">
<ol className="relative text-gray-500 border-s border-gray-200 dark:border-gray-700 dark:text-gray-400 ">
{[
{
step: 1,
title: "Copy Setup Prompt",
content: <div className="flex min-w-0 flex-col gap-4">
<CodeBlock
language="text"
content={selectedInstallPrompt}
customRender={
<pre className="max-h-[480px] overflow-y-auto whitespace-pre-wrap break-words p-4 text-sm leading-6 text-foreground">
{selectedInstallPrompt}
</pre>
}
title="Prompt for your AI agent"
icon="code"
maxHeight={480}
/>
</div>,
},
{
step: 2,
title: "Create Keys",
content: <>
<Typography>
Add this server-only key to your project&apos;s <InlineCode>.env.local</InlineCode> file.
</Typography>
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} />
</>,
},
{
step: 3,
title: "Done",
content: <SetupRecommendedDoneStep onExploreDashboard={props.toMetrics} />,
},
].map((item) => (
<li key={item.step} className={cn("ms-6 flex flex-col lg:flex-row gap-10 mb-20")}>
<div className="flex flex-col justify-center gap-2 max-w-[180px] min-w-[180px]">
<span className={`absolute flex items-center justify-center w-8 h-8 bg-zinc-100 dark:bg-zinc-800 rounded-full -start-4 ring-4 ring-white dark:ring-zinc-900`}>
<span className={`text-zinc-500 dark:text-zinc-400 font-semibold`}>{item.step}</span>
</span>
<h3 className="font-medium leading-tight">{item.title}</h3>
</div>
</div>,
},
...(selectedFramework === 'nextjs' ? nextJsSteps : []),
...(selectedFramework === 'tanstackStart' ? tanstackStartSteps : []),
...(selectedFramework === 'react' ? reactSteps : []),
...(selectedFramework === 'javascript' ? javascriptSteps : []),
...(selectedFramework === 'python' ? pythonSteps : []),
].map((item, index) => (
<li key={item.step} className={cn("ms-6 flex flex-col lg:flex-row gap-10 mb-20")}>
<div className="flex flex-col justify-center gap-2 max-w-[180px] min-w-[180px]">
<span className={`absolute flex items-center justify-center w-8 h-8 bg-gray-100 dark:bg-gray-70 rounded-full -start-4 ring-4 ring-white dark:ring-gray-900`}>
<span className={`text-gray-500 dark:text-gray-700 font-medium`}>{item.step}</span>
</span>
<h3 className="font-medium leading-tight">{item.title}</h3>
</div>
<div className="flex flex-grow flex-col gap-4">
{item.content}
</div>
</li>
))}
</ol>
</div>
<div className="flex min-w-0 flex-grow flex-col gap-4">
{item.content}
</div>
</li>
))}
</ol>
</div>
) : (
<div className="mx-4 mt-12 flex flex-col items-center gap-4 py-16 text-center">
<Typography>
Manual setup steps live in the documentation so they stay up to date with every framework and SDK change.
</Typography>
<DesignButton
onClick={() => {
window.open(manualSetupDocsUrl, '_blank');
}}
>
<BookIcon className="w-4 h-4 mr-2" />
Open manual setup docs
</DesignButton>
</div>
)}
</PageLayout>
);
}
@ -813,45 +295,44 @@ function GlobeIllustrationInner() {
);
}
function SetupRecommendedDoneStep(props: { onExploreDashboard: () => void }) {
return (
<div className="flex flex-col gap-4">
<Typography>
Hooray! Setup completed.
</Typography>
<div>
<DesignButton onClick={props.onExploreDashboard}>
Explore Dashboard
</DesignButton>
</div>
</div>
);
}
function HexclaveKeys(props: {
keys: { projectId: string, publishableClientKey?: string, secretServerKey: string } | null,
onGenerateKeys: () => Promise<void>,
type: 'next' | 'vite' | 'raw',
}) {
return (
<div className="w-full border rounded-xl p-8 gap-4 flex flex-col">
{props.keys ? (
<>
{props.type === 'next' ? (
<NextJsEnvKeys
projectId={props.keys.projectId}
publishableClientKey={props.keys.publishableClientKey}
secretServerKey={props.keys.secretServerKey}
/>
) : props.type === 'vite' ? (
<ViteEnvKeys
projectId={props.keys.projectId}
secretServerKey={props.keys.secretServerKey}
/>
) : (
<APIEnvKeys
projectId={props.keys.projectId}
publishableClientKey={props.keys.publishableClientKey}
secretServerKey={props.keys.secretServerKey}
/>
)}
<Typography type="label" variant="secondary">
{`Save these keys securely - they won't be shown again after leaving this page.`}
</Typography>
</>
) : (
if (!props.keys) {
return (
<div className={cn(codePanelShellClasses, "w-full p-5 flex flex-col")}>
<div className="flex items-center justify-center">
<DesignButton onClick={props.onGenerateKeys}>
Generate Keys
</DesignButton>
</div>
)}
</div>
);
}
return (
<div className="w-full flex flex-col gap-3">
<EnvFileViewer filename=".env.local" value={`HEXCLAVE_SECRET_SERVER_KEY=${props.keys.secretServerKey}`} />
<Typography type="label" variant="secondary">
{`Save these keys securely - they won't be shown again after leaving this page.`}
</Typography>
</div>
);
}

View File

@ -17,6 +17,10 @@ Object.entries({ tsx, bash, typescript, python, sql }).forEach(([key, value]) =>
SyntaxHighlighter.registerLanguage(key, value);
});
export const codePanelShellClasses = "overflow-hidden transition-all duration-150 hover:transition-none rounded-xl bg-white/90 dark:bg-background/60 dark:backdrop-blur-xl ring-1 ring-black/[0.06] hover:ring-black/[0.1] dark:ring-white/[0.06] dark:hover:ring-white/[0.1] shadow-none border border-black/[0.06] dark:border-white/[0.06]";
export const codePanelHeaderClasses = "text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center bg-black/[0.015] dark:bg-white/[0.015] py-2.5 border-b border-black/[0.06] dark:border-white/[0.06]";
type CodeBlockProps = {
language: string,
content: string,
@ -32,7 +36,7 @@ type CodeBlockProps = {
};
export function CodeBlock(props: CodeBlockProps) {
const { theme, mounted } = useThemeWatcher();
const { theme } = useThemeWatcher();
let icon = null;
switch (props.icon) {
@ -47,8 +51,20 @@ export function CodeBlock(props: CodeBlockProps) {
}
return (
<div className={cn("overflow-hidden", !props.fullWidth && "rounded-xl", props.neutralBackground ? "bg-background" : "bg-muted")}>
<div className={cn("text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center", props.compact && !props.noSeparator && "py-1", !props.compact && !props.noSeparator && "py-2", props.noSeparator && "pt-1 pb-0", !props.noSeparator && "border-b")}>
<div className={cn(
props.neutralBackground
? "overflow-hidden transition-all duration-150 hover:transition-none rounded-xl bg-background border border-black/[0.08] dark:border-white/[0.06] shadow-sm"
: !props.fullWidth
? codePanelShellClasses
: "overflow-hidden transition-all duration-150 hover:transition-none bg-white/90 dark:bg-background/60 dark:backdrop-blur-xl ring-1 ring-black/[0.06] hover:ring-black/[0.1] dark:ring-white/[0.06] dark:hover:ring-white/[0.1] shadow-none border border-black/[0.06] dark:border-white/[0.06]"
)}>
<div className={cn(
"text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center bg-black/[0.015] dark:bg-white/[0.015]",
props.compact && !props.noSeparator && "py-1.5",
!props.compact && !props.noSeparator && "py-2.5",
props.noSeparator && "pt-1 pb-0",
!props.noSeparator && "border-b border-black/[0.06] dark:border-white/[0.06]"
)}>
<h5 className={cn("font-medium flex items-center gap-2", props.compact && "text-xs")}>
{icon}
{props.title}

View File

@ -1,7 +1,86 @@
import { getPublicEnvVar } from '@/lib/env';
import { Button, CopyField, Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui";
"use client";
function getEnvFileContent(props: {
import { codePanelHeaderClasses, codePanelShellClasses } from '@/components/code-block';
import { getPublicEnvVar } from '@/lib/env';
import { Button, CopyButton, CopyField, Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui";
import React, { useState } from "react";
import { EyeIcon, EyeSlashIcon, FileTextIcon } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
type EnvFileViewerProps = {
filename: string;
value: string;
}
export function EnvFileViewer({ filename, value }: EnvFileViewerProps) {
const [revealAll, setRevealAll] = useState(false);
const lines = value.split("\n").map((line, idx) => {
const eqIndex = line.indexOf("=");
if (eqIndex === -1) return { key: `comment_${idx}`, val: line, isComment: true };
const key = line.substring(0, eqIndex);
const val = line.substring(eqIndex + 1);
return { key, val, isComment: false };
});
return (
<div className={cn(codePanelShellClasses, "w-full flex flex-col")}>
<div className={codePanelHeaderClasses}>
<h5 className="font-medium flex items-center gap-2">
<FileTextIcon className="w-4 h-4" />
{filename}
</h5>
<div className="flex items-center gap-2">
<Button
type="button"
variant="secondary"
className="h-6 w-6 p-1"
onClick={() => setRevealAll(!revealAll)}
title={revealAll ? "Mask values" : "Reveal values"}
aria-label={revealAll ? "Mask values" : "Reveal values"}
>
{revealAll ? <EyeSlashIcon className="h-3.5 w-3.5" /> : <EyeIcon className="h-3.5 w-3.5" />}
</Button>
<CopyButton content={value} variant="secondary" />
</div>
</div>
<div className="overflow-x-auto p-4 font-mono text-xs select-text">
<table className="w-full border-collapse">
<tbody>
{lines.map((line, idx) => {
return (
<tr key={idx} className="group hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors leading-relaxed">
<td className="py-0.5 w-full">
<div className="flex items-center justify-between gap-4 w-full">
<div className="flex items-center flex-wrap whitespace-pre">
{line.isComment ? (
<span className="text-muted-foreground/50 italic">{line.val}</span>
) : (
<>
<span className="text-indigo-600 dark:text-indigo-400 font-medium select-all">{line.key}</span>
<span className="text-muted-foreground/50 mx-1">=</span>
{revealAll ? (
<span className="text-teal-600 dark:text-teal-400 font-medium break-all select-all">{line.val}</span>
) : (
<span className="text-muted-foreground/45 tracking-[0.25em] font-sans text-xs select-none"></span>
)}
</>
)}
</div>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
export function getEnvFileContent(props: {
projectId: string,
publishableClientKey?: string,
secretServerKey?: string,
@ -91,6 +170,7 @@ export function APIEnvKeys(props: {
<CopyField
type="input"
monospace
isSecret
value={props.secretServerKey}
label="Secret Server Key"
helper="This key is used on the server-side and can be used to perform actions on behalf of your users. Keep it safe."
@ -100,6 +180,7 @@ export function APIEnvKeys(props: {
<CopyField
type="input"
monospace
isSecret
value={props.superSecretAdminKey}
label="Super Secret Admin Key"
helper="This key is for administrative use only. Anyone owning this key will be able to create unlimited new keys and revoke any other keys. Be careful!"
@ -118,13 +199,7 @@ export function NextJsEnvKeys(props: {
const envFileContent = getEnvFileContent(props);
return (
<CopyField
type="textarea"
monospace
height={envFileContent.split("\n").length * 26}
value={envFileContent}
fixedSize
/>
<EnvFileViewer filename=".env.local" value={envFileContent} />
);
}
@ -142,12 +217,6 @@ export function ViteEnvKeys(props: {
.join("\n");
return (
<CopyField
type="textarea"
monospace
height={envFileContent.split("\n").length * 26}
value={envFileContent}
fixedSize
/>
<EnvFileViewer filename=".env" value={envFileContent} />
);
}

View File

@ -1,8 +1,12 @@
"use client";
import React, { useState } from "react";
import { Input } from "./input";
import { Label } from "./label";
import { SimpleTooltip } from "./simple-tooltip";
import { Textarea } from "./textarea";
import { CopyButton } from "./copy-button";
import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
export function CopyField(props: {
value: string,
@ -11,12 +15,15 @@ export function CopyField(props: {
monospace?: boolean,
fixedSize?: boolean,
initialCopied?: boolean,
isSecret?: boolean,
} & ({
type: "textarea",
height?: number,
} | {
type: "input",
})) {
const [isRevealed, setIsRevealed] = useState(false);
return (
<div>
{props.label && (
@ -43,12 +50,24 @@ export function CopyField(props: {
<div className="flex items-center gap-2">
<Input
readOnly
type={props.isSecret && !isRevealed ? "password" : "text"}
value={props.value}
className={props.isSecret ? "font-mono pr-10" : undefined}
style={{
fontFamily: props.monospace ? "ui-monospace, monospace" : "inherit",
}}
/>
<CopyButton content={props.value} initialCopied={props.initialCopied} />
{props.isSecret && (
<button
type="button"
onClick={() => setIsRevealed(!isRevealed)}
className="h-9 w-9 flex items-center justify-center rounded-xl border border-black/[0.08] dark:border-white/[0.06] bg-white/80 dark:bg-foreground/[0.03] shadow-sm text-muted-foreground/60 hover:text-foreground hover:bg-white dark:hover:bg-foreground/[0.06] transition-all shrink-0"
title={isRevealed ? "Hide key" : "Show key"}
>
{isRevealed ? <EyeSlashIcon className="h-4 w-4" /> : <EyeIcon className="h-4 w-4" />}
</button>
)}
<CopyButton content={props.value} initialCopied={props.initialCopied} className="h-9 w-9 p-1.5 rounded-xl border border-black/[0.08] dark:border-white/[0.06] bg-white/80 dark:bg-foreground/[0.03] shadow-sm hover:bg-white dark:hover:bg-foreground/[0.06] transition-all shrink-0" />
</div>
)}
</div>

View File

@ -10,7 +10,7 @@ const Textarea = forwardRefIfNeeded<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
"stack-scope flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
"stack-scope flex min-h-[60px] w-full rounded-xl border border-black/[0.08] dark:border-white/[0.06] bg-white/80 dark:bg-foreground/[0.03] shadow-sm ring-1 ring-black/[0.08] dark:ring-white/[0.06] placeholder:text-muted-foreground/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/[0.1] disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-150 hover:transition-none hover:bg-white dark:hover:bg-foreground/[0.06] px-3 py-2 text-sm",
className
)}
ref={ref}