> ## Documentation Index
> Fetch the complete documentation index at: https://docs.whop.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Embed Whop chat in your app in minutes

export const code = {
  auth: {
    token: [{
      code: `import { NextResponse } from "next/server";

// [step:1.1:start]
// Retrieve user id via your own auth/session system.
// For a quick test, you can hardcode them.
const USER_ID = "user_XXXXXXXXXXXX";
const COMPANY_ID = "biz_XXXXXXXXXXXXX";
// [step:1.1:end]

export async function POST() {
  // [step:1.2:start]
  const response = await fetch("https://api.whop.com/api/v1/access_tokens", {
    method: "POST",
    headers: {
      Authorization: \`Bearer \${process.env.WHOP_API_KEY}\`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      company_id: COMPANY_ID,
      user_id: USER_ID,
      scoped_actions: [
        "chat:read", // view messages in experience chats
        "chat:message:create", // post messages in experience chats
        "dms:read", // view DM threads
        "dms:message:manage", // send / edit / delete DMs
        "dms:channel:manage", // create / update DM channels
        "support_chat:read", // view the user's own support chat
        "support_chat:message:create", // post in support chats
      ],
    }),
  });

  const data = await response.json();
  return NextResponse.json({ token: data.token });
  // [step:1.2:end]
}
`,
      filename: "app/api/chat/token/route.ts",
      language: "typescript",
      conditions: [{
        category: "backend",
        value: "nextjs"
      }]
    }, {
      code: `import express from "express";

const app = express();
app.use(express.json());

// [step:1.1:start]
// Retrieve user id via your own auth/session system.
// For a quick test, you can hardcode them.
const USER_ID = "user_XXXXXXXXXXXX";
const COMPANY_ID = "biz_XXXXXXXXXXXXX";
// [step:1.1:end]

app.post("/api/chat/token", async (_req, res) => {
	// [step:1.2:start]
	const response = await fetch("https://api.whop.com/api/v1/access_tokens", {
		method: "POST",
		headers: {
			Authorization: \`Bearer \${process.env.WHOP_API_KEY}\`,
			"Content-Type": "application/json",
		},
		body: JSON.stringify({
			company_id: COMPANY_ID,
			user_id: USER_ID,
			scoped_actions: [
				"chat:read", // view messages in experience chats
				"chat:message:create", // post messages in experience chats
				"dms:read", // view DM threads
				"dms:message:manage", // send / edit / delete DMs
				"dms:channel:manage", // create / update DM channels
				"support_chat:read", // view the user's own support chat
				"support_chat:message:create", // post in support chats
			],
		}),
	});

	const data = await response.json();
	res.json({ token: data.token });
	// [step:1.2:end]
});

app.listen(3000, () => console.log("Server running on port 3000"));
`,
      filename: "server.ts",
      language: "typescript",
      conditions: [{
        category: "backend",
        value: "express"
      }]
    }, {
      code: `<!doctype html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Chat</title>
        <!-- [step:2.1] -->
        <script src="https://apollo.elements.whop.com/release/elements.js"></script>
        <style>
            * {
                margin: 0;
                padding: 0;
                box-sizing: border-box;
            }
        </style>
    </head>
    <body>
        <div id="chat-container" style="height: 100vh; width: 100%"></div>

        <script>
            // [step:2.2]
            const elements = new WhopElements();

            // [step:2.3:start]
            // Called by the session on mount and again before the token expires,
            // so it must always return a fresh token.
            async function getToken() {
                const res = await fetch("/api/chat/token", { method: "POST" });
                const data = await res.json();
                if (!data.token) throw new Error("Failed to fetch token");
                return data.token;
            }

            const session = elements.createChatSession({ token: getToken });
            // [step:2.3:end]

            // [step:2.4:start]
            const chatElement = session.createElement("chat-element", {
                channelId: "chat_XXXXXXXXXXXXXX",
            });

            chatElement.mount("#chat-container");
            // [step:2.4:end]
        </script>
    </body>
</html>
`,
      filename: "chat.html",
      language: "html",
      conditions: [{
        category: "client",
        value: "html"
      }]
    }, {
      code: `// [step:2.3:start]
import {
	// [step:2.3:end]
	// [step:2.4]
	ChatElement,
	ChatSession,
	Elements,
	// [step:2.3]
} from "@whop/embedded-components-react-js";
// [step:2.2]
import { loadWhopElements } from "@whop/embedded-components-vanilla-js";
// [step:2.4]
import type { ChatElementOptions } from "@whop/embedded-components-vanilla-js/types";
import { useMemo } from "react";

// [step:2.2]
const elements = loadWhopElements();

// [step:2.3:start]
// Called by ChatSession on mount and again before the token expires,
// so it must always return a fresh token — don't cache a stale one.
async function getToken() {
	const res = await fetch("/api/chat/token", { method: "POST" });
	const data = await res.json();
	if (!data.token) throw new Error("Failed to fetch token");
	return data.token;
}
// [step:2.3:end]

export function Chat() {
	// [step:2.4:start]
	const chatOptions: ChatElementOptions = useMemo(() => {
		return {
			channelId: "chat_XXXXXXXXXXXXXX",
		};
	}, []);
	// [step:2.4:end]

	return (
		// [step:2.3:start]
		<Elements elements={elements}>
			<ChatSession token={getToken}>
				{/* [step:2.3:end] */}
				{/* [step:2.4:start] */}
				<ChatElement
					options={chatOptions}
					style={{ height: "100dvh", width: "100%" }}
				/>
				{/* [step:2.4:end] */}
			</ChatSession>
		</Elements>
	);
}
`,
      filename: "chat.tsx",
      language: "tsx",
      conditions: [{
        category: "client",
        value: "react"
      }]
    }, {
      code: `import SwiftUI

// [step:2.1]
import WhopElements

struct ContentView: View {
	var body: some View {
		NavigationStack {
			// [step:2.4]
			WhopChatView(
				channelId: "chat_XXXXXXXXXXXXXX",
				style: .imessage
			)
		}
		// [step:2.2:start]
		.task {
			await WhopSDK.configure(tokenProvider: WhopAPITokenProvider())
		}
		// [step:2.2:end]
	}
}

// [step:2.2:start]
class WhopAPITokenProvider: WhopTokenProvider {
	private let serverURL = URL(string: "https://your-server.com")!

	func getToken() async -> WhopTokenResponse {
		do {
			var req = URLRequest(url: serverURL.appendingPathComponent("api/chat/token"))
			req.httpMethod = "POST"

			let (data, _) = try await URLSession.shared.data(for: req)
			let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
			return WhopTokenResponse(accessToken: json["token"] as? String ?? "")
		} catch {
			return WhopTokenResponse(accessToken: "")
		}
	}
}
// [step:2.2:end]
`,
      filename: "ChatView.swift",
      language: "swift",
      conditions: [{
        category: "client",
        value: "swift"
      }]
    }],
    "better-auth": [{
      code: `# [step:1.2:start]
# Your Whop OAuth app client ID
# Get yours at: https://whop.com/dashboard/developer
WHOP_CLIENT_ID=

# Secret key for encryption
# Generate one with: openssl rand -base64 32
BETTER_AUTH_SECRET=

# Base URL of your app
BETTER_AUTH_URL=http://localhost:3000
# [step:1.2:end]
`,
      filename: ".env",
      language: "bash",
      conditions: [{
        category: "backend",
        value: "express"
      }, {
        category: "client",
        value: ["react", "html"]
      }]
    }, {
      code: `# [step:1.2:start]
# Your Whop OAuth app client ID
# Get yours at: https://whop.com/dashboard/developer
WHOP_CLIENT_ID=

# Secret key for encryption
# Generate one with: openssl rand -base64 32
BETTER_AUTH_SECRET=

# Base URL of your app
BETTER_AUTH_URL=http://localhost:3000
# [step:1.2:end]
`,
      filename: ".env",
      language: "bash",
      conditions: [{
        category: "backend",
        value: "nextjs"
      }, {
        category: "client",
        value: ["react", "html"]
      }]
    }, {
      code: `# [step:1.2:start]
# Your Whop OAuth app client ID
# Get yours at: https://whop.com/dashboard/developer
WHOP_CLIENT_ID=

# Secret key for encryption
# Generate one with: openssl rand -base64 32
BETTER_AUTH_SECRET=
# [step:1.2:end]
`,
      filename: ".env",
      language: "bash",
      conditions: [{
        category: "client",
        value: "html"
      }]
    }, {
      code: `// [step:1.4:start]

import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";

export const auth = betterAuth({
	account: {
		storeStateStrategy: "cookie",
		storeAccountCookie: true,
	},
	plugins: [
		genericOAuth({
			config: [
				{
					providerId: "whop",
					clientId: process.env.WHOP_CLIENT_ID ?? "",
					authorizationUrl: "https://api.whop.com/oauth/authorize",
					tokenUrl: "https://api.whop.com/oauth/token",
					userInfoUrl: "https://api.whop.com/oauth/userinfo",
					scopes: [
						"openid",
						"profile",
						"email",
						"chat:message:create",
						"chat:read",
						"dms:read",
						"dms:message:manage",
						"dms:channel:manage",
						"support_chat:read",
						"support_chat:message:create",
					],
					pkce: true,
					authorizationUrlParams: () => ({
						nonce: crypto.randomUUID(),
					}),
				},
			],
		}),
	],
});
// [step:1.4:end]
`,
      filename: "auth.ts",
      language: "typescript",
      conditions: [{
        category: "client",
        value: ["react", "html"]
      }]
    }, {
      code: `import { genericOAuthClient } from "better-auth/client/plugins";
// [step:1.5:start]
import { createAuthClient } from "better-auth/react";

export const authClient = createAuthClient({
	plugins: [genericOAuthClient()],
});
// [step:1.5:end]
`,
      filename: "auth-client.ts",
      language: "typescript",
      conditions: [{
        category: "client",
        value: "react"
      }]
    }, {
      code: `// [step:1.6:start]
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";

export const { GET, POST } = toNextJsHandler(auth);
// [step:1.6:end]
`,
      filename: "api/auth/[...all]/route.ts",
      language: "typescript",
      conditions: [{
        category: "backend",
        value: "nextjs"
      }, {
        category: "client",
        value: ["react", "html"]
      }]
    }, {
      code: `import { toNodeHandler } from "better-auth/node";
// [step:1.6:start]
import express from "express";
import { auth } from "./auth";

const app = express();
const port = 3000;

app.all("/api/auth/*", toNodeHandler(auth)); // For ExpressJS v4
// app.all("/api/auth/*splat", toNodeHandler(auth)); For ExpressJS v5

// Mount express json middleware after Better Auth handler
// or only apply it to routes that don't interact with Better Auth
app.use(express.json());

app.listen(port, () => {
	console.log(\`App listening on port \${port}\`);
});
// [step:1.6:end]
`,
      filename: "server.ts",
      language: "typescript",
      conditions: [{
        category: "backend",
        value: "express"
      }, {
        category: "client",
        value: ["react", "html"]
      }]
    }, {
      code: `<!-- [step:1.7:start] -->
<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <button onclick="handleSignIn()">Sign in</button>

    <script>
      async function handleSignIn() {
        const res = await fetch("/api/auth/sign-in/oauth2", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            providerId: "whop",
            callbackURL: "/chat.html",
          }),
        });
        const data = await res.json();
        if (data.url) {
          window.location.href = data.url;
        }
      }
    </script>
  </body>
</html>
<!-- [step:1.7:end] -->
`,
      filename: "sign-in.html",
      language: "html",
      conditions: [{
        category: "client",
        value: "html"
      }]
    }, {
      code: `// [step:1.7:start]
import { authClient } from "@/lib/auth-client";

export function SignIn() {
	function handleSignIn() {
		authClient.signIn.oauth2({
			providerId: "whop",
			callbackURL: "/chat",
		});
	}

	return (
		<button type="button" onClick={handleSignIn}>
			Sign in
		</button>
	);
}
// [step:1.7:end]
`,
      filename: "sign-in.tsx",
      language: "tsx",
      conditions: [{
        category: "client",
        value: "react"
      }]
    }, {
      code: `<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Chat</title>
    <!-- [step:2.1] -->
    <script src="https://apollo.elements.whop.com/release/elements.js"></script>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
    </style>
  </head>
  <body>
    <div id="chat-container" style="height: 100vh; width: 100%;"></div>

    <script>
      // [step:2.2]
      const elements = new WhopElements();

      // [step:2.3:start]
      async function getToken() {
        const res = await fetch("/api/auth/get-access-token", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ providerId: "whop" }),
        });

        const data = await res.json();

        if (data.error) {
          throw new Error("Failed to fetch token");
        }

        return data.accessToken;
      }

      const session = elements.createChatSession({ token: getToken });
      // [step:2.3:end]

      // [step:2.4:start]
      const chatElement = session.createElement("chat-element", {
        channelId: "chat_XXXXXXXXXXXXXX",
      });

      chatElement.mount("#chat-container");
      // [step:2.4:end]
    </script>
  </body>
</html>
`,
      filename: "chat.html",
      language: "html",
      conditions: [{
        category: "client",
        value: "html"
      }]
    }, {
      code: `// [step:2.3]
import { authClient } from "@/lib/auth-client";
// [step:2.3:start]
import {
	// [step:2.3:end]
	// [step:2.4]
	ChatElement,
	ChatSession,
	Elements,
	// [step:2.3]
} from "@whop/embedded-components-react-js";
// [step:2.2]
import { loadWhopElements } from "@whop/embedded-components-vanilla-js";
// [step:2.4]
import type { ChatElementOptions } from "@whop/embedded-components-vanilla-js/types";
import { useMemo } from "react";

// [step:2.2]
const elements = loadWhopElements();

// [step:2.3:start]
async function getToken() {
	const { data, error } = await authClient.getAccessToken({
		providerId: "whop",
	});

	if (error) {
		throw new Error("Failed to fetch token");
	}

	return data.accessToken;
}
// [step:2.3:end]

export function Chat() {
	// [step:2.4:start]
	const chatOptions: ChatElementOptions = useMemo(() => {
		return {
			channelId: "chat_XXXXXXXXXXXXXX",
		};
	}, []);
	// [step:2.4:end]

	return (
		// [step:2.3:start]
		<Elements elements={elements}>
			<ChatSession token={getToken}>
				{/* [step:2.3:end] */}
				{/* [step:2.4:start] */}
				<ChatElement
					options={chatOptions}
					style={{ height: "100dvh", width: "100%" }}
				/>
				{/* [step:2.4:end] */}
			</ChatSession>
		</Elements>
	);
}
`,
      filename: "chat.tsx",
      language: "tsx",
      conditions: [{
        category: "client",
        value: "react"
      }]
    }, {
      code: `import SwiftUI

// [step:2.1]
import WhopElements

struct ContentView: View {
	var body: some View {
		NavigationStack {
			// [step:2.4]
			WhopChatView(
				channelId: "chat_XXXXXXXXXXXXXX",
				style: .imessage
			)
		}
		// [step:2.2:start]
		.task {
			await WhopSDK.configureWithOAuth(
				appId: "app_XXXXXXXXXXXXXX",
				scopes: [
					"openid", "profile", "email",
					"chat:message:create", "chat:read",
					"dms:read", "dms:message:manage", "dms:channel:manage",
					"support_chat:read", "support_chat:message:create",
				]
			)
		}
		// [step:2.2:end]
	}
}
`,
      filename: "ChatView.swift",
      language: "swift",
      conditions: [{
        category: "client",
        value: "swift"
      }]
    }],
    authjs: [{
      code: `# [step:1.2:start]
# Your Whop OAuth app client ID
# Get yours at: https://whop.com/dashboard/developer
WHOP_CLIENT_ID=

# Secret key for encryption
# Generate one with: npx auth secret
AUTH_SECRET=
# [step:1.2:end]
`,
      filename: ".env",
      language: "bash",
      conditions: [{
        category: "client",
        value: ["react", "html"]
      }]
    }, {
      code: `// [step:1.4:start]
import type { DefaultSession, ExpressAuthConfig } from "@auth/express";

declare module "@auth/express" {
	interface Session extends DefaultSession {
		accessToken?: string;
	}
}

export const authConfig: ExpressAuthConfig = {
	providers: [
		{
			id: "whop",
			name: "Whop",
			type: "oauth",
			checks: ["pkce", "state"],
			client: {
				token_endpoint_auth_method: "none",
			},
			authorization: {
				url: "https://api.whop.com/oauth/authorize",
				params: {
					scope:
						"openid profile email chat:message:create chat:read dms:read dms:message:manage dms:channel:manage support_chat:read support_chat:message:create",
					nonce: crypto.randomUUID(),
				},
			},
			token: {
				url: "https://api.whop.com/oauth/token",
				async conform(response: Response) {
					const body = await response.json();
					const { id_token: _, ...rest } = body;
					return new Response(JSON.stringify(rest), {
						status: response.status,
						headers: { "Content-Type": "application/json" },
					});
				},
			},
			userinfo: "https://api.whop.com/oauth/userinfo",
			clientId: process.env.WHOP_CLIENT_ID,
			profile(profile) {
				return {
					id: profile.sub,
					name: profile.name,
					email: profile.email,
					image: profile.picture,
				};
			},
		},
	],
	session: {
		strategy: "jwt",
	},
	callbacks: {
		async jwt({ token, account }) {
			if (account?.access_token) {
				token.accessToken = account.access_token;
			}
			return token;
		},
		async session({ session, token }) {
			if (token.accessToken) {
				session.accessToken = token.accessToken as string;
			}
			return session;
		},
	},
};
// [step:1.4:end]
`,
      filename: "auth.ts",
      language: "typescript",
      conditions: [{
        category: "backend",
        value: "express"
      }, {
        category: "client",
        value: ["react", "html"]
      }]
    }, {
      code: `// [step:1.4:start]
import NextAuth, { type DefaultSession } from "next-auth";
import "next-auth/jwt";

declare module "next-auth" {
	interface Session extends DefaultSession {
		accessToken?: string;
	}
}

declare module "next-auth/jwt" {
	interface JWT {
		accessToken?: string;
	}
}

export const { handlers, auth, signIn } = NextAuth({
	providers: [
		{
			id: "whop",
			name: "Whop",
			type: "oauth",
			checks: ["pkce", "state"],
			client: {
				token_endpoint_auth_method: "none",
			},
			authorization: {
				url: "https://api.whop.com/oauth/authorize",
				params: {
					scope:
						"openid profile email chat:message:create chat:read dms:read dms:message:manage dms:channel:manage support_chat:read support_chat:message:create",
					nonce: crypto.randomUUID(),
				},
			},
			token: {
				url: "https://api.whop.com/oauth/token",
				async conform(response: Response) {
					const body = await response.json();
					const { id_token: _, ...rest } = body;
					return new Response(JSON.stringify(rest), {
						status: response.status,
						headers: { "Content-Type": "application/json" },
					});
				},
			},
			userinfo: "https://api.whop.com/oauth/userinfo",
			clientId: process.env.WHOP_CLIENT_ID,
			profile(profile) {
				return {
					id: profile.sub,
					name: profile.name,
					email: profile.email,
					image: profile.picture,
				};
			},
		},
	],
	session: {
		strategy: "jwt",
	},
	callbacks: {
		async jwt({ token, account }) {
			if (account?.access_token) {
				token.accessToken = account.access_token;
			}
			return token;
		},
		async session({ session, token }) {
			if (token.accessToken) {
				session.accessToken = token.accessToken;
			}
			return session;
		},
	},
});
// [step:1.4:end]
`,
      filename: "auth.ts",
      language: "typescript",
      conditions: [{
        category: "backend",
        value: "nextjs"
      }, {
        category: "client",
        value: ["react", "html"]
      }]
    }, {
      code: `// [step:1.6:start]
import { handlers } from "@/auth";

export const { GET, POST } = handlers;
// [step:1.6:end]
`,
      filename: "api/auth/[...all]/route.ts",
      language: "typescript",
      conditions: [{
        category: "backend",
        value: "nextjs"
      }, {
        category: "client",
        value: ["react", "html"]
      }]
    }, {
      code: `import { ExpressAuth } from "@auth/express";
// [step:1.6:start]
import express from "express";
import { authConfig } from "./auth";

const app = express();
const port = 3000;

app.use("/api/auth/*", ExpressAuth(authConfig));

app.listen(port, () => {
	console.log(\`App listening on port \${port}\`);
});
// [step:1.6:end]
`,
      filename: "server.ts",
      language: "typescript",
      conditions: [{
        category: "backend",
        value: "express"
      }, {
        category: "client",
        value: ["react", "html"]
      }]
    }, {
      code: `// [step:1.7:start]
export function SignIn() {
	async function handleSignIn() {
		const csrfRes = await fetch("/api/auth/csrf");
		const csrf = await csrfRes.json();

		const res = await fetch("/api/auth/signin/whop", {
			method: "POST",
			headers: {
				"Content-Type": "application/x-www-form-urlencoded",
				"X-Auth-Return-Redirect": "1",
			},
			body: new URLSearchParams({
				csrfToken: csrf.csrfToken,
				callbackUrl: "/chat",
			}),
		});

		const data = await res.json();
		if (data.url) {
			window.location.href = data.url;
		}
	}

	return (
		<button type="button" onClick={handleSignIn}>
			Sign in
		</button>
	);
}
// [step:1.7:end]
`,
      filename: "sign-in.tsx",
      language: "tsx",
      conditions: [{
        category: "client",
        value: "react"
      }, {
        category: "backend",
        value: "express"
      }]
    }, {
      code: `<!-- [step:1.7:start] -->
<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <button onclick="handleSignIn()">Sign in</button>

    <script>
      async function handleSignIn() {
        const csrfRes = await fetch("/api/auth/csrf");
        const csrf = await csrfRes.json();

        const res = await fetch("/api/auth/signin/whop", {
          method: "POST",
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
            "X-Auth-Return-Redirect": "1",
          },
          body: new URLSearchParams({
            csrfToken: csrf.csrfToken,
            callbackUrl: "/chat.html",
          }),
        });

        const data = await res.json();
        if (data.url) {
          window.location.href = data.url;
        }
      }
    </script>
  </body>
</html>
<!-- [step:1.7:end] -->
`,
      filename: "sign-in.html",
      language: "html",
      conditions: [{
        category: "client",
        value: "html"
      }]
    }, {
      code: `// [step:1.7:start]
import { signIn } from "next-auth/react";

export function SignIn() {
	function handleSignIn() {
		signIn("whop", { callbackUrl: "/chat" });
	}

	return (
		<button type="button" onClick={handleSignIn}>
			Sign in
		</button>
	);
}
// [step:1.7:end]
`,
      filename: "sign-in.tsx",
      language: "tsx",
      conditions: [{
        category: "client",
        value: "react"
      }, {
        category: "backend",
        value: "nextjs"
      }]
    }, {
      code: `<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Chat</title>
    <!-- [step:2.1] -->
    <script src="https://apollo.elements.whop.com/release/elements.js"></script>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
    </style>
  </head>
  <body>
    <div id="chat-container" style="height: 100vh; width: 100%;"></div>

    <script>
      // [step:2.2]
      const elements = new WhopElements();

      // [step:2.3:start]
      async function getToken() {
        return fetch("/api/auth/session")
          .then((res) => res.json())
          .then((data) => data.accessToken);
      }

      const session = elements.createChatSession({ token: getToken });
      // [step:2.3:end]

      // [step:2.4:start]
      const chatElement = session.createElement("chat-element", {
        channelId: "chat_XXXXXXXXXXXXXX",
      });

      chatElement.mount("#chat-container");
      // [step:2.4:end]
    </script>
  </body>
</html>
`,
      filename: "chat.html",
      language: "html",
      conditions: [{
        category: "client",
        value: "html"
      }]
    }, {
      code: `// [step:2.3:start]
import {
	// [step:2.3:end]
	// [step:2.4]
	ChatElement,
	ChatSession,
	Elements,
	// [step:2.3]
} from "@whop/embedded-components-react-js";
// [step:2.2]
import { loadWhopElements } from "@whop/embedded-components-vanilla-js";
// [step:2.4]
import type { ChatElementOptions } from "@whop/embedded-components-vanilla-js/types";
import { useMemo } from "react";

// [step:2.2]
const elements = loadWhopElements();

// [step:2.3:start]
async function getToken() {
	return fetch("/api/auth/session")
		.then((res) => res.json())
		.then((data) => data.accessToken);
}
// [step:2.3:end]

export function Chat() {
	// [step:2.4:start]
	const chatOptions: ChatElementOptions = useMemo(() => {
		return {
			channelId: "chat_XXXXXXXXXXXXXX",
		};
	}, []);
	// [step:2.4:end]

	return (
		// [step:2.3:start]
		<Elements elements={elements}>
			<ChatSession token={getToken}>
				{/* [step:2.3:end] */}
				{/* [step:2.4:start] */}
				<ChatElement
					options={chatOptions}
					style={{ height: "100dvh", width: "100%" }}
				/>
				{/* [step:2.4:end] */}
			</ChatSession>
		</Elements>
	);
}
`,
      filename: "chat.tsx",
      language: "tsx",
      conditions: [{
        category: "client",
        value: "react"
      }]
    }, {
      code: `import SwiftUI

// [step:2.1]
import WhopElements

struct ContentView: View {
	var body: some View {
		NavigationStack {
			// [step:2.4]
			WhopChatView(
				channelId: "chat_XXXXXXXXXXXXXX",
				style: .imessage
			)
		}
		// [step:2.2:start]
		.task {
			await WhopSDK.configureWithOAuth(
				appId: "app_XXXXXXXXXXXXXX",
				scopes: [
					"openid", "profile", "email",
					"chat:message:create", "chat:read",
					"dms:read", "dms:message:manage", "dms:channel:manage",
					"support_chat:read", "support_chat:message:create",
				]
			)
		}
		// [step:2.2:end]
	}
}
`,
      filename: "ChatView.swift",
      language: "swift",
      conditions: [{
        category: "client",
        value: "swift"
      }]
    }]
  },
  backend: {
    nextjs: [{
      code: `import { type NextRequest, NextResponse } from "next/server";

// [step:1.1:start]
import Whop from "@whop/sdk";

const whop = new Whop({
	appId: process.env.WHOP_APP_ID,
	apiKey: process.env.WHOP_API_KEY,
});
// [step:1.1:end]

export async function GET(request: NextRequest) {
	const code = request.nextUrl.searchParams.get("code");

	if (!code) {
		return new Response(null, { status: 400 });
	}

	// [step:1.2:start]
	const tokenResponse = await whop.oauth.validateCode({
		code,
		redirect_uri: "http://localhost:3000/api/auth/callback",
	});

	const tokens = JSON.stringify({
		access_token: tokenResponse.access_token,
		refresh_token: tokenResponse.refresh_token,
		expires_in: tokenResponse.expires_in,
		obtained_at: Date.now(),
	});

	const response = NextResponse.redirect(new URL("/", request.url));
	response.cookies.set("whop_tokens", tokens, {
		maxAge: 30 * 24 * 60 * 60,
		sameSite: "strict",
		path: "/",
	});

	return response;
	// [step:1.2:end]
}
`,
      filename: "route.ts",
      language: "typescript"
    }],
    express: [{
      code: `// [step:1.1]
import Whop from "@whop/sdk";
import express from "express";

const app = express();
app.use(express.json());

// [step:1.1:start]
const whop = new Whop({
	appId: process.env.WHOP_APP_ID,
	apiKey: process.env.WHOP_API_KEY,
});
// [step:1.1:end]

app.get("/api/auth/callback", async (req, res) => {
	const code = req.query.code as string;

	if (!code) {
		return res.status(400).json({ error: "code is required" });
	}

	// [step:1.2:start]
	const tokenResponse = await whop.oauth.validateCode({
		code,
		redirect_uri: "http://localhost:3000/api/auth/callback",
	});

	const tokens = JSON.stringify({
		access_token: tokenResponse.access_token,
		refresh_token: tokenResponse.refresh_token,
		expires_in: tokenResponse.expires_in,
		obtained_at: Date.now(),
	});

	res.cookie("whop_tokens", tokens, {
		maxAge: 30 * 24 * 60 * 60 * 1000,
		sameSite: "strict",
		path: "/",
	});

	res.redirect("/");
	// [step:1.2:end]
});

app.listen(3000, () => console.log("Server running on port 3000"));
`,
      filename: "server.ts",
      language: "typescript"
    }]
  },
  client: {
    react: [{
      code: `// [step:2.3:start]
import {
	// [step:2.3:end]
	// [step:2.4]
	ChatElement,
	ChatSession,
	Elements,
	// [step:2.3]
} from "@whop/embedded-components-react-js";
// [step:2.2]
import { loadWhopElements } from "@whop/embedded-components-vanilla-js";
// [step:2.4]
import type { ChatElementOptions } from "@whop/embedded-components-vanilla-js/types";
import { useMemo } from "react";

// [step:2.2]
const elements = loadWhopElements();

// [step:2.3:start]
async function getToken() {
	// Replaced by auth-specific getToken when an auth library is selected
	throw new Error("Configure an auth library to get a token");
}
// [step:2.3:end]

export function Chat() {
	// [step:2.4:start]
	const chatOptions: ChatElementOptions = useMemo(() => {
		return {
			channelId: "chat_XXXXXXXXXXXXXX",
		};
	}, []);
	// [step:2.4:end]

	return (
		// [step:2.3:start]
		<Elements elements={elements}>
			<ChatSession token={getToken}>
				{/* [step:2.3:end] */}
				{/* [step:2.4:start] */}
				<ChatElement
					options={chatOptions}
					style={{ height: "100dvh", width: "100%" }}
				/>
				{/* [step:2.4:end] */}
			</ChatSession>
		</Elements>
	);
}
`,
      filename: "chat.tsx",
      language: "tsx"
    }],
    html: [{
      code: `<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Chat</title>
    <!-- [step:2.1] -->
    <script src="https://apollo.elements.whop.com/release/elements.js"></script>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
    </style>
  </head>
  <body>
    <div id="chat-container" style="height: 100vh; width: 100%;"></div>

    <script>
      // [step:2.2]
      const elements = new WhopElements();

      // [step:2.3:start]
      async function getToken() {
        // Replaced by auth-specific getToken when an auth library is selected
        throw new Error("Configure an auth library to get a token");
      }

      const session = elements.createChatSession({ token: getToken });
      // [step:2.3:end]

      // [step:2.4:start]
      const chatElement = session.createElement("chat-element", {
        channelId: "chat_XXXXXXXXXXXXXX",
      });

      chatElement.mount("#chat-container");
      // [step:2.4:end]
    </script>
  </body>
</html>
`,
      filename: "chat.html",
      language: "html"
    }],
    swift: [{
      code: `import SwiftUI

// [step:2.1]
import WhopElements

struct ContentView: View {
    var body: some View {
        NavigationStack {
            // [step:2.3]
            WhopChatView(
                channelId: "chat_XXXXXXXXXXXXXX",
                style: .imessage
            )
        }
        // [step:2.2:start]
        .task {
            await WhopSDK.configureWithOAuth(
                appId: "app_XXXXXXXXXXXXXX",
                scopes: [
                    "openid", "profile", "email",
                    "chat:message:create", "chat:read",
                    "dms:read", "dms:message:manage", "dms:channel:manage",
                    "support_chat:read", "support_chat:message:create",
                ]
            )
        }
        // [step:2.2:end]
    }
}
`,
      filename: "ChatView.swift",
      language: "swift"
    }]
  }
};

export const QuickStart = ({children, code, steps, title, description, platformGroups, conditionalCategories}) => {
  const CATEGORY_CONFIG = {
    frontend: {
      label: "Client",
      languages: {
        react: {
          label: "React"
        },
        html: {
          label: "HTML"
        }
      }
    },
    client: {
      label: "Client",
      languages: {
        react: {
          label: "React"
        },
        html: {
          label: "Vanilla"
        },
        swift: {
          label: "Swift"
        }
      }
    },
    backend: {
      label: "Server",
      languages: {
        express: {
          label: "Express"
        },
        nextjs: {
          label: "Next.js"
        },
        python: {
          label: "Python"
        },
        ruby: {
          label: "Ruby"
        },
        go: {
          label: "Go"
        }
      }
    },
    auth: {
      label: "Auth",
      languages: {
        token: {
          label: "Token"
        },
        "better-auth": {
          label: "Better Auth"
        },
        authjs: {
          label: "Auth.js"
        }
      }
    }
  };
  const allCategories = Object.keys(code);
  const [activePlatformGroupId, setActivePlatformGroupId] = React.useState(() => platformGroups?.[0]?.id ?? null);
  const [selectedLanguages, setSelectedLanguages] = React.useState(() => Object.entries(code).reduce((acc, [category, languages]) => {
    acc[category] = Object.keys(languages)[0];
    return acc;
  }, {}));
  const activePlatformGroup = platformGroups?.find(g => g.id === activePlatformGroupId);
  const conditionalCatNames = new Set(conditionalCategories ? Object.keys(conditionalCategories) : []);
  const platformCategories = activePlatformGroup ? allCategories.filter(c => activePlatformGroup.categories.includes(c)) : allCategories.filter(c => !conditionalCatNames.has(c));
  const activeConditionalCats = [];
  const hiddenByConditional = new Set();
  if (conditionalCategories) {
    const matchesShowWhen = cond => Object.entries(cond).every(([key, value]) => {
      if (!allCategories.includes(key)) return false;
      if (Array.isArray(value)) return value.includes(selectedLanguages[key]);
      return selectedLanguages[key] === value;
    });
    for (const [cat, config] of Object.entries(conditionalCategories)) {
      if (!allCategories.includes(cat)) continue;
      const isActive = Array.isArray(config.showWhen) ? config.showWhen.some(matchesShowWhen) : matchesShowWhen(config.showWhen);
      if (isActive) {
        activeConditionalCats.push(cat);
        for (const hidden of config.hides || []) {
          hiddenByConditional.add(hidden);
        }
      }
    }
  }
  const selectorCategories = [...platformCategories, ...activeConditionalCats];
  const matchableCategories = [...new Set([...selectorCategories, ...hiddenByConditional])];
  const fileCategories = selectorCategories.filter(c => !hiddenByConditional.has(c));
  const categories = fileCategories;
  const [selectedFileIndex, setSelectedFileIndex] = React.useState(0);
  const [activeSubStepKey, setActiveSubStepKey] = React.useState(null);
  const [isCodeContentHovered, setIsCodeContentHovered] = React.useState(false);
  const subStepRefs = React.useRef({});
  const stepsContainerRef = React.useRef(null);
  const codePanelRef = React.useRef(null);
  const lastAutoSwitchedStep = React.useRef(null);
  const isClickScrolling = React.useRef(false);
  React.useEffect(() => {
    const container = stepsContainerRef.current;
    if (!container) return;
    const handleScroll = () => {
      if (isClickScrolling.current) return;
      const scrollTop = container.scrollTop;
      let newActiveKey = null;
      const keys = Object.keys(subStepRefs.current).sort((a, b) => {
        const [aStep, aSub] = a.split("-").map(Number);
        const [bStep, bSub] = b.split("-").map(Number);
        return aStep - bStep || aSub - bSub;
      });
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i];
        const el = subStepRefs.current[key];
        if (!el) continue;
        const elTop = el.offsetTop - container.offsetTop;
        const elHeight = el.offsetHeight;
        const scrollPastThreshold = elHeight * 0.15;
        if (scrollTop < elTop + scrollPastThreshold) {
          newActiveKey = key;
          break;
        }
      }
      setActiveSubStepKey(newActiveKey);
    };
    container.addEventListener("scroll", handleScroll);
    return () => container.removeEventListener("scroll", handleScroll);
  }, [steps]);
  const handlePlatformGroupSelect = groupId => {
    setActivePlatformGroupId(groupId);
    setSelectedFileIndex(0);
    lastAutoSwitchedStep.current = null;
  };
  const handleLanguageSelect = (category, language) => {
    setSelectedLanguages(prev => ({
      ...prev,
      [category]: language
    }));
    lastAutoSwitchedStep.current = null;
  };
  const getFiles = (category, language) => {
    const langCode = code[category]?.[language];
    if (!langCode) return [];
    return Array.isArray(langCode) ? langCode : [langCode];
  };
  const allFiles = (() => {
    const raw = categories.flatMap(category => {
      const language = selectedLanguages[category];
      return getFiles(category, language).filter(file => {
        if (!file.conditions) return true;
        return file.conditions.every(c => {
          if (Array.isArray(c.value)) {
            return c.value.includes(selectedLanguages[c.category]);
          }
          return selectedLanguages[c.category] === c.value;
        });
      }).map(file => ({
        ...file,
        category
      }));
    });
    if (activeConditionalCats.length === 0) return raw;
    const result = [];
    const seen = new Set();
    for (let i = raw.length - 1; i >= 0; i--) {
      if (!seen.has(raw[i].filename)) {
        seen.add(raw[i].filename);
        result.unshift(raw[i]);
      }
    }
    return result;
  })();
  React.useEffect(() => {
    if (selectedFileIndex >= allFiles.length) {
      setSelectedFileIndex(0);
    }
  }, [allFiles.length]);
  const processedFiles = allFiles.map(file => {
    const code = file.code;
    const lines = code.split("\n");
    let lineOffset = 0;
    const markers = [];
    let openMarkers = [];
    const filteredLines = [];
    const singleLinePattern = /(?:\/\/|#|\/\*|<!--)\s*\[step:(\d+\.\d+)\]\s*(?:\*\/|-->)?/;
    const multiLineStartPattern = /(?:\/\/|#|\/\*|<!--)\s*\[step:(\d+\.\d+):start\]\s*(?:\*\/|-->)?/;
    const multiLineEndPattern = /(?:\/\/|#|\/\*|<!--)\s*\[step:(\d+\.\d+):end\]\s*(?:\*\/|-->)?/;
    for (const lineIdx in lines) {
      const line = lines[lineIdx];
      const matchesSingleLine = line.match(singleLinePattern);
      if (matchesSingleLine) {
        const currentLine = parseInt(lineIdx) - lineOffset + 1;
        markers.push({
          step: matchesSingleLine[1],
          range: [currentLine, currentLine]
        });
        lineOffset += 1;
        continue;
      }
      const matchesMultiLineStart = line.match(multiLineStartPattern);
      if (matchesMultiLineStart) {
        openMarkers.push({
          step: matchesMultiLineStart[1],
          start: parseInt(lineIdx) - lineOffset + 1
        });
        lineOffset += 1;
        continue;
      }
      const matchesMultiLineEnd = line.match(multiLineEndPattern);
      if (matchesMultiLineEnd) {
        const start = [...openMarkers].reverse().find(marker => marker.step === matchesMultiLineEnd[1]);
        if (start) {
          openMarkers = openMarkers.filter(marker => marker !== start);
          markers.push({
            step: matchesMultiLineEnd[1],
            range: [start.start, parseInt(lineIdx) - lineOffset]
          });
          lineOffset += 1;
          continue;
        }
      }
      filteredLines.push(line);
    }
    return {
      ...file,
      code: filteredLines.join("\n"),
      markers
    };
  });
  const getActiveStepMarkerId = () => {
    if (!activeSubStepKey) return null;
    const [stepIdx, subStepIdx] = activeSubStepKey.split("-").map(Number);
    return `${stepIdx + 1}.${subStepIdx + 1}`;
  };
  React.useEffect(() => {
    if (!activeSubStepKey || lastAutoSwitchedStep.current === activeSubStepKey) return;
    const stepMarkerId = getActiveStepMarkerId();
    if (!stepMarkerId) return;
    const fileIndex = processedFiles.findIndex(file => file.markers.some(m => m.step === stepMarkerId));
    if (fileIndex !== -1) {
      lastAutoSwitchedStep.current = activeSubStepKey;
      setSelectedFileIndex(fileIndex);
    }
  }, [activeSubStepKey, selectedLanguages]);
  React.useEffect(() => {
    const timer = setTimeout(() => {
      const container = codePanelRef.current?.querySelector('[data-component-part="code-block-root"]');
      const highlightedLine = container?.querySelector(".line-highlight, .line-focus");
      if (container && highlightedLine) {
        const lineTop = highlightedLine.offsetTop;
        const containerHeight = container.clientHeight;
        const lineHeight = highlightedLine.offsetHeight;
        container.scrollTo({
          top: lineTop - containerHeight / 2 + lineHeight / 2,
          behavior: "smooth"
        });
      }
    }, 50);
    return () => clearTimeout(timer);
  }, [activeSubStepKey, selectedFileIndex]);
  const currentFile = processedFiles[selectedFileIndex] || processedFiles[0];
  const getHighlightLines = () => {
    if (!currentFile || !activeSubStepKey) return null;
    const stepMarkerId = getActiveStepMarkerId();
    const stepMarkers = currentFile.markers.filter(m => m.step === stepMarkerId);
    if (stepMarkers.length === 0) return null;
    const lines = [];
    for (const marker of stepMarkers) {
      for (let i = marker.range[0]; i <= marker.range[1]; i++) {
        lines.push(i);
      }
    }
    return JSON.stringify(lines);
  };
  const highlightLines = getHighlightLines();
  const focusLines = isCodeContentHovered ? null : highlightLines;
  return <div className="qs-container" id="quickstart-content">
			<div className="qs-top-bar">
				{platformGroups && <div className="qs-selector-group">
						<span className="qs-selector-label">Platform</span>
						<div className="qs-selector-buttons">
							{platformGroups.map(group => <button key={group.id} onClick={() => handlePlatformGroupSelect(group.id)} className={`qs-selector-button ${activePlatformGroupId === group.id ? "qs-selector-button-active" : ""}`} aria-pressed={activePlatformGroupId === group.id}>
									{group.label}
								</button>)}
						</div>
						<select className="qs-selector-select" value={activePlatformGroupId} onChange={e => handlePlatformGroupSelect(e.target.value)}>
							{platformGroups.map(group => <option key={group.id} value={group.id}>
									{group.label}
								</option>)}
						</select>
					</div>}
				{selectorCategories.map(category => {
    const config = CATEGORY_CONFIG[category] || ({
      label: category,
      languages: {}
    });
    const languages = code[category];
    const languageKeys = Object.keys(languages);
    const selectedLanguage = selectedLanguages[category];
    if (languageKeys.length <= 1) return null;
    return <div key={category} className="qs-selector-group">
							<span className="qs-selector-label">{config.label}</span>
							<div className="qs-selector-buttons">
								{languageKeys.map(language => {
      const langConfig = config.languages[language] || ({
        label: language
      });
      const isSelected = selectedLanguage === language;
      return <button key={language} onClick={() => handleLanguageSelect(category, language)} className={`qs-selector-button ${isSelected ? "qs-selector-button-active" : ""}`} aria-pressed={isSelected}>
											{langConfig.label}
										</button>;
    })}
							</div>
							<select className="qs-selector-select" value={selectedLanguage} onChange={e => handleLanguageSelect(category, e.target.value)}>
								{languageKeys.map(language => {
      const langConfig = config.languages[language] || ({
        label: language
      });
      return <option key={language} value={language}>
											{langConfig.label}
										</option>;
    })}
							</select>
						</div>;
  })}
			</div>
			<div className="qs-steps" ref={stepsContainerRef}>
				<div className="px-6 py-2 flex flex-col gap-1">
					<h1 className="m-0 text-2xl sm:text-3xl font-bold text-gray-900 tracking-tight dark:text-gray-200">
						{title}
					</h1>
					{description}
				</div>
				{(() => {
    let stepNumber = 0;
    return steps.map((step, idx) => {
      const resolvedSubSteps = step.subSteps.map(subStep => {
        if (!Array.isArray(subStep)) return subStep;
        return subStep.find(s => Object.entries(s.match).every(([key, value]) => matchableCategories.includes(key) && (Array.isArray(value) ? value.includes(selectedLanguages[key]) : selectedLanguages[key] === value)));
      }).filter(Boolean);
      if (resolvedSubSteps.length === 0) return null;
      stepNumber++;
      const displayNumber = stepNumber;
      return <div key={idx} className="qs-step-group">
								<div className="qs-step-header">
									<span className="qs-step-indicator">{displayNumber}</span>
									<h3>{step.title}</h3>
								</div>
								<div>
									{step.subSteps.map((subStep, subIdx) => {
        const subStepKey = `${idx}-${subIdx}`;
        const isSubStepActive = activeSubStepKey === subStepKey;
        const resolvedSubStep = Array.isArray(subStep) ? subStep.find(s => Object.entries(s.match).every(([key, value]) => matchableCategories.includes(key) && (Array.isArray(value) ? value.includes(selectedLanguages[key]) : selectedLanguages[key] === value))) : subStep;
        if (!resolvedSubStep) return null;
        const handleStepClick = () => {
          if (isSubStepActive) return;
          setActiveSubStepKey(subStepKey);
          isClickScrolling.current = true;
          const el = subStepRefs.current[subStepKey];
          const container = stepsContainerRef.current;
          if (el && container) {
            const elTop = el.offsetTop - container.offsetTop;
            container.scrollTo({
              top: elTop,
              behavior: "smooth"
            });
          }
          setTimeout(() => {
            isClickScrolling.current = false;
          }, 500);
        };
        return <div className={`qs-step ${isSubStepActive ? "qs-step-active" : ""}`} key={subIdx} ref={el => subStepRefs.current[subStepKey] = el} onClick={handleStepClick} style={{
          cursor: isSubStepActive ? "default" : "pointer"
        }}>
												<h4 className="mb-2">{resolvedSubStep.title}</h4>
												<div className="qs-step-content">
													{resolvedSubStep.content}
												</div>
											</div>;
      })}
								</div>
							</div>;
    });
  })()}
				<div className="px-6 pb-6 ml-1">{children}</div>
			</div>
			<div className="qs-code-panel" ref={codePanelRef}>
				<div className="qs-code-header">
					{allFiles.length > 1 ? <div className="qs-file-tabs">
							{allFiles.map((file, index) => <button key={`${file.category}-${file.language}-${file.filename}`} onClick={() => setSelectedFileIndex(index)} className={`qs-file-tab ${index === selectedFileIndex ? "qs-file-tab-active" : ""}`}>
									{file.filename}
								</button>)}
						</div> : <span className="qs-code-filename">
							{currentFile?.filename || "No files"}
						</span>}
				</div>
				<div className="qs-code-content" onMouseEnter={() => setIsCodeContentHovered(true)} onMouseLeave={() => setIsCodeContentHovered(false)}>
					{currentFile ? <CodeBlock lines={true} language={currentFile.language} focus={focusLines} highlight={highlightLines}>
							{currentFile.code}
						</CodeBlock> : "Select a language"}
				</div>
			</div>
		</div>;
};

export const ExamplesLink = ({href}) => <a href={href} className="examples-link">
		<div className="examples-link__row">
			<span className="examples-link__title">✨ Build this with AI</span>
			<span className="examples-link__jump">
				<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
					<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" />
					<polyline points="15 3 21 3 21 9" />
					<line x1="10" y1="14" x2="21" y2="3" />
				</svg>
			</span>
		</div>
		<p className="examples-link__description">
			Copy and paste our templates using your favorite editor
		</p>
		<div className="examples-link__icons">
			<span className="examples-link__icon" title="Claude">
				<svg viewBox="0 0 24 24">
					<path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fill="#D97757" fillRule="nonzero" />
				</svg>
			</span>
			<span className="examples-link__icon" title="Cursor">
				<svg viewBox="85 69 343 354" fill="currentColor">
					<path d="M255.428 423l148.991-83.5L255.428 256l-148.99 83.5 148.99 83.5z" fillOpacity=".7" />
					<path d="M404.419 339.5v-167L255.428 89v167l148.991 83.5z" fillOpacity=".5" />
					<path d="M255.428 89l-148.99 83.5v167l148.99-83.5V89z" fillOpacity=".6" />
					<path d="M404.419 172.5L255.428 423V256l148.991-83.5z" fillOpacity=".9" />
					<path d="M404.419 172.5L255.428 256l-148.99-83.5h297.981z" />
				</svg>
			</span>
			<span className="examples-link__icon" title="Codex">
				<svg viewBox="0 0 24 24" fill="#10a37f">
					<path d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z" />
				</svg>
			</span>
			<span className="examples-link__icon" title="Lovable">
				<svg viewBox="0 0 24 24">
					<path clipRule="evenodd" d="M7.082 0c3.91 0 7.081 3.179 7.081 7.1v2.7h2.357c3.91 0 7.082 3.178 7.082 7.1 0 3.923-3.17 7.1-7.082 7.1H0V7.1C0 3.18 3.17 0 7.082 0z" fill="url(#lovable-grad)" fillRule="evenodd" />
					<defs>
						<radialGradient cx="0" cy="0" gradientTransform="matrix(-1 22.5 -30.454 -1.354 14 3)" gradientUnits="userSpaceOnUse" id="lovable-grad" r="1">
							<stop offset=".25" stopColor="#FE7B02" />
							<stop offset=".433" stopColor="#FE4230" />
							<stop offset=".548" stopColor="#FE529A" />
							<stop offset=".654" stopColor="#DD67EE" />
							<stop offset=".95" stopColor="#4B73FF" />
						</radialGradient>
					</defs>
				</svg>
			</span>
			<span className="examples-link__more">+ more</span>
		</div>
	</a>;

<QuickStart
  code={code}
  conditionalCategories={{
backend: { showWhen: [{ client: ["react", "html"] }, { client: "swift", auth: "token" }] },
auth: { showWhen: {}, hides: ["backend", "client"] },
}}
  steps={[{
title: "Set up authentication",
subSteps: [[
  {
    match: { auth: "token" },
    title: "Get your API credentials",
    content: <>
      <p>You'll need three things:</p>
      <ol>
        <li>
          <h5><strong>API key</strong></h5>
          <p>Create one at <a href="https://whop.com/dashboard/developer">Whop Developer Dashboard</a>. Treat it like a password — never expose it in client code.</p>
        </li>
        <li>
          <h5><strong>Company ID</strong></h5>
          <p>Find it in your dashboard URL: <code>whop.com/dashboard/<strong>biz_XXXXXXXXX</strong>/</code>.</p>
        </li>
        <li>
          <h5><strong>User ID</strong></h5>
          <p>The user you want to log in as. In production, you'll derive this from your own auth system.</p>
        </li>
      </ol>
      <p><em>Both <code>company_id</code> and <code>user_id</code> are required — together they scope the token to a specific user acting inside your company.</em></p>
    </>
  }, {
    match: { auth: "better-auth", client: ["react", "html"] },
    title: "Install Better Auth",
    content: <>
      Install <a href="https://better-auth.com">Better Auth</a> in your project:

      <CodeGroup>
        <CodeBlock language="bash" filename="npm">
          npm install better-auth
        </CodeBlock>
        <CodeBlock language="bash" filename="pnpm">
          pnpm add better-auth
        </CodeBlock>
      </CodeGroup>

    </>
  }, {
    match: { auth: "authjs", backend: "nextjs", client: ["react", "html"] },
    title: "Install Auth.js",
    content: <>
      Install <a href="https://authjs.dev">Auth.js</a> in your project:

      <CodeGroup>
        <CodeBlock language="bash" filename="npm">
          npm install next-auth@beta
        </CodeBlock>
        <CodeBlock language="bash" filename="pnpm">
          pnpm add next-auth@beta
        </CodeBlock>
      </CodeGroup>
    </>
  }, {
    match: { auth: "authjs", backend: "express", client: ["react", "html"] },
    title: "Install Auth.js",
    content: <>
      Install <a href="https://authjs.dev">Auth.js</a> in your project:

      <CodeGroup>
        <CodeBlock language="bash" filename="npm">
          npm install @auth/express
        </CodeBlock>
        <CodeBlock language="bash" filename="pnpm">
          pnpm add @auth/express
        </CodeBlock>
      </CodeGroup>
    </>
  }
], [
  {
    match: { auth: "token" },
    title: "Create a token endpoint on your server",
    content: <>
      <p>Add a route that mints a short-lived access token for a user using your API key. The client calls this endpoint to get a token — your API key stays on the server.</p>
      <p>Set <code>WHOP_API_KEY</code> in your <code>.env</code> file. Replace <code>USER_ID</code> and <code>COMPANY_ID</code> with the IDs from the previous step. Trim <code>scoped_actions</code> to only what you need.</p>
    </>
  }, {
    match: { auth: "better-auth", client: "react" },
    title: "Set environment variables",
    content: <>
      <p>Create a <code>.env</code> file in the root of your project and add the following environment variables:</p>
      <ol>
        <li>
          <h5><code>WHOP_CLIENT_ID</code></h5>
          <p>Your Whop OAuth app client ID. Get yours at <a href="https://whop.com/dashboard/developer">Whop Developer Dashboard</a>.</p>
        </li>
        <li>
          <h5><code>BETTER_AUTH_SECRET</code></h5>
          <p>A secret key used for encryption and hashing. Generate one with <code>openssl rand -base64 32</code>.</p>
        </li>
        <li>
          <h5><code>BETTER_AUTH_URL</code></h5>
          <p>The base URL of your app.</p>
        </li>
      </ol>
    </>
  }, {
    match: { auth: "better-auth", client: "html" },
    title: "Set environment variables",
    content: <>
      <p>Create a <code>.env</code> file in the root of your project and add the following environment variables:</p>
      <ol>
        <li>
          <h5><code>WHOP_CLIENT_ID</code></h5>
          <p>Your Whop OAuth app client ID. Get yours at <a href="https://whop.com/dashboard/developer">Whop Developer Dashboard</a>.</p>
        </li>
        <li>
          <h5><code>BETTER_AUTH_SECRET</code></h5>
          <p>A secret key used for encryption and hashing. Generate one with <code>openssl rand -base64 32</code>.</p>
        </li>
      </ol>
    </>
  }, {
    match: { auth: "authjs", client: ["react", "html"] },
    title: "Set environment variables",
    content: <>
      <p>Create a <code>.env</code> file in the root of your project and add the following environment variables:</p>
      <ol>
        <li>
          <h5><code>WHOP_CLIENT_ID</code></h5>
          <p>Your Whop OAuth app client ID. Get yours at <a href="https://whop.com/dashboard/developer">Whop Developer Dashboard</a>.</p>
        </li>
        <li>
          <h5><code>AUTH_SECRET</code></h5>
          <p>A secret key used for encryption. Generate one with <code>npx auth secret</code>.</p>
        </li>
      </ol>
    </>
  }
], [
  {
    match: { auth: "better-auth", client: ["react", "html"] },
    title: "Set the redirect URI",
    content: <>
      <p>In your <a href="https://whop.com/dashboard/developer">Whop OAuth app settings</a>, add the following redirect URIs:</p>
      <ul>
        <li><code>http://localhost:3000/api/auth/oauth2/callback/whop</code> (local development)</li>
        <li><code>https://your-domain.com/api/auth/oauth2/callback/whop</code> (production)</li>
      </ul>
    </>
  }, {
    match: { auth: "authjs", client: ["react", "html"] },
    title: "Set the redirect URI",
    content: <>
      <p>In your <a href="https://whop.com/dashboard/developer">Whop OAuth app settings</a>, add the following redirect URIs:</p>
      <ul>
        <li><code>http://localhost:3000/api/auth/callback/whop</code> (local development)</li>
        <li><code>https://your-domain.com/api/auth/callback/whop</code> (production)</li>
      </ul>
    </>
  }
], [
  {
    match: { auth: "better-auth", client: ["react", "html"] },
    title: "Create an auth instance",
    content: <>
      <p>Create a file called <code>auth.ts</code>. Configure Whop as a generic OAuth provider using the <code>genericOAuth</code> plugin.</p>
    </>
  },       {
    match: { auth: "authjs", client: ["react", "html"] },
    title: "Create an auth instance",
    content: <>
      <p>Create a file called <code>auth.ts</code>. Configure Whop as a custom OAuth provider.</p>
    </>
  }
], [
  {
    match: { auth: "better-auth", client: "react" },
    title: "Create an auth client instance",
    content: <>
      <p>Create a file called <code>auth-client.ts</code> and add the <code>genericOAuthClient</code> plugin. The client-side auth instance helps you interact with the auth server.</p>
    </>
  }
], [
  {
    match: { auth: "better-auth", backend: "nextjs", client: ["react", "html"] },
    title: "Mount the handler",
    content: <>
      <p>To handle API requests, set up a catch-all route handler at <code>app/api/auth/[...all]/route.ts</code>. Better Auth handles the full OAuth flow including token exchange and session creation.</p>
    </>
  }, {
    match: { auth: "better-auth", backend: "express", client: ["react", "html"] },
    title: "Mount the handler",
    content: <>
      <p>To handle API requests, mount Better Auth on your Express server using <code>toNodeHandler</code>. This handles the full OAuth flow at <code>/api/auth/*</code>.</p>
    </>
  },       {
    match: { auth: "authjs", backend: "nextjs", client: ["react", "html"] },
    title: "Mount the handler",
    content: <>
      <p>To handle API requests, set up a catch-all route handler at <code>app/api/auth/[...all]/route.ts</code>.</p>
    </>
  }, {
    match: { auth: "authjs", backend: "express", client: ["react", "html"] },
    title: "Mount the handler",
    content: <>
      <p>Mount Auth.js on your Express server using <code>ExpressAuth</code>.</p>
    </>
  }
], [
  {
    match: { auth: "better-auth", client: "react" },
    title: "Create a sign-in page",
    content: <>
      <p>Create a sign-in page and have the user sign in.</p>
    </>
  }, {
    match: { auth: "better-auth", client: "html" },
    title: "Create a sign-in page",
    content: <>
      <p>Create a sign-in page and have the user sign in.</p>
    </>
  }, {
    match: { auth: "authjs", client: "react" },
    title: "Create a sign-in page",
    content: <>
      <p>Create a sign-in page and have the user sign in.</p>
    </>
  }, {
    match: { auth: "authjs", client: "html" },
    title: "Create a sign-in page",
    content: <>
      <p>Create a sign-in page and have the user sign in.</p>
    </>
  }
]]

}, {
title: "Set up chat",
subSteps: [[
{
match: { client: "react" },
title: "Install the embedded components",
content: <>
Install the Whop embedded components packages for React.

      <CodeGroup>
        <CodeBlock language="bash" filename="npm">
          npm install @whop/embedded-components-react-js@beta @whop/embedded-components-vanilla-js@beta
        </CodeBlock>
        <CodeBlock language="bash" filename="pnpm">
          pnpm add @whop/embedded-components-react-js@beta @whop/embedded-components-vanilla-js@beta
        </CodeBlock>
      </CodeGroup>
    </>
  },       {
    match: { client: "html" },
    title: "Add the script tag",
    content: <>
      Include the Whop Elements script in your HTML page.

      <CodeBlock language="html">
        {`<script src="https://apollo.elements.whop.com/release/elements.js"></script>`}
      </CodeBlock>
    </>
  }, {
    match: { client: "swift" },
    title: "Add the Swift package",
    content: <>
      In Xcode, go to <strong>File</strong> → <strong>Add Package Dependencies...</strong> and enter the package URL:

      <CodeBlock language="text">
        https://github.com/whopio/whopsdk-elements-swift
      </CodeBlock>

      <p>Then add <code>WhopElements</code> to your target. Chat supports sending photos, videos, and voice messages. Add the following keys to your app's <code>Info.plist</code> so iOS prompts the user for access:</p>

      <CodeBlock language="xml" filename="Info.plist">

{`<key>NSCameraUsageDescription</key>

<string>Allow camera access to take photos and videos in chat</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow microphone access to record voice messages in chat</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow photo library access to share photos and videos in chat</string>`}
      </CodeBlock>
    </>
  }
], [
  {
    match: { client: "react" },
    title: "Load the elements",
    content: <>
      <p>Call <code>loadWhopElements()</code> to initialize the Whop elements.</p>
    </>
  }, {
    match: { client: "html" },
    title: "Initialize the elements",
    content: <>
      <p>Create a new <code>WhopElements</code> instance to initialize the Whop elements.</p>
    </>
  }, {
    match: { auth: "token", client: "swift" },
    title: "Create a token provider",
    content: <>
      <p>Implement <code>WhopTokenProvider</code> and call your token endpoint to get a short-lived access token. Pass the provider to <code>WhopSDK.configure</code> on app launch — the SDK will call <code>getToken()</code> again automatically when the token expires.</p>
      <p>Point <code>serverURL</code> at the backend you set up in the previous step (e.g., a production URL or a local tunnel in development).</p>
    </>
  }, {
    match: { auth: ["better-auth", "authjs"], client: "swift" },
    title: "Configure OAuth",
    content: <>
      <p>Create an OAuth app via the <a href="/api-reference/apps/create-app">Create App API</a> or in the <a href="https://whop.com/dashboard/">Whop Dashboard</a> → Developer. See the <a href="/developer/guides/oauth">OAuth guide</a> for both methods.</p>
      <ol>
        <li>Set your redirect URI (e.g., <code>com.yourapp.bundle://oauth/callback</code>).</li>
        <li>Enable the required scopes: <code>openid</code>, <code>profile</code>, <code>email</code>, <code>chat:message:create</code>, <code>chat:read</code>, <code>dms:read</code>, <code>dms:message:manage</code>, <code>dms:channel:manage</code>, <code>support_chat:read</code>, <code>support_chat:message:create</code>.</li>
      </ol>
      <p>Call <code>configureWithOAuth</code> on app launch. The SDK handles the full OAuth flow, sign-in UI, and token management automatically. If you already have tokens from your backend or want to skip the SDK's OAuth flow, see the <a href="/developer/guides/chat/authentication">authentication guide</a> for pre-filling tokens and manual sign-in.</p>
    </>
  }
], [
  {
    match: { auth: "token", client: ["react", "html"] },
    title: "Add the ChatSession",
    content: <>
      <p>The session fetches an access token from your token endpoint.</p>
      <ul>
        <li><code>token</code> - Function that returns the token string. It's called again automatically when the token expires.</li>
      </ul>
    </>
  }, {
    match: { auth: "better-auth", client: ["react", "html"] },
    title: "Add the ChatSession",
    content: <>
      <p>The session handles authentication using the token from your auth server.</p>
      <ul>
        <li><code>token</code> - Function that fetches the access token from your server</li>
      </ul>
    </>
  }, {
    match: { auth: "authjs", client: ["react", "html"] },
    title: "Add the ChatSession",
    content: <>
      <p>The session handles authentication using the token from your auth server.</p>
      <ul>
        <li><code>token</code> - Function that fetches the access token from your server</li>
      </ul>
    </>
  }
], [
  {
    match: { client: "react" },
    title: "Add the ChatElement",
    content: <>
      <p>The element connects to the channel and renders a real-time chat UI.</p>
      <p>A <code>channelId</code> starts with <code>chat_</code>. Get one by creating (or fetching) a channel via the API — pick the type that fits your use case:</p>
      <ul>
        <li><a href="/api-reference/chat-channels/chat-channel"><strong>Chat channel</strong></a> — free-form group chat attached to an experience.</li>
        <li><a href="/api-reference/dm-channels/dm-channel"><strong>DM channel</strong></a> — 1:1 or small-group direct messages between users.</li>
        <li><a href="/api-reference/support-channels/support-channel"><strong>Support channel</strong></a> — 1:1 customer support between a user and your company (only one per user).</li>
      </ul>
    </>
  }, {
    match: { client: "html" },
    title: "Add the ChatElement",
    content: <>
      <p>The element connects to the channel and renders a real-time chat UI.</p>
      <p>A <code>channelId</code> starts with <code>chat_</code>. Get one by creating (or fetching) a channel via the API — pick the type that fits your use case:</p>
      <ul>
        <li><a href="/api-reference/chat-channels/chat-channel"><strong>Chat channel</strong></a> — free-form group chat attached to an experience.</li>
        <li><a href="/api-reference/dm-channels/dm-channel"><strong>DM channel</strong></a> — 1:1 or small-group direct messages between users.</li>
        <li><a href="/api-reference/support-channels/support-channel"><strong>Support channel</strong></a> — 1:1 customer support between a user and your company (only one per user).</li>
      </ul>
    </>
  }, {
    match: { client: "swift" },
    title: "Add WhopChatView",
    content: <>
      <p>Place <code>WhopChatView</code> in your view hierarchy with a channel ID. The SDK handles authentication, rendering, and real-time updates. Choose between <code>.imessage</code> (bubble chat) and <code>.discord</code> (full-width) styles.</p>
      <p>A channel ID starts with <code>chat_</code>. Get one by creating (or fetching) a channel via the API — pick the type that fits your use case:</p>
      <ul>
        <li><a href="/api-reference/chat-channels/chat-channel"><strong>Chat channel</strong></a> — free-form group chat attached to an experience.</li>
        <li><a href="/api-reference/dm-channels/dm-channel"><strong>DM channel</strong></a> — 1:1 or small-group direct messages between users.</li>
        <li><a href="/api-reference/support-channels/support-channel"><strong>Support channel</strong></a> — 1:1 customer support between a user and your company (only one per user).</li>
      </ul>
    </>
  }
]]
}]}
  title="Embed chat in your app"
  description={<>
Integrate Whop's real-time messaging directly into your web or iOS app.
<ExamplesLink href="https://whop.com/network/examples?filter=chat" />
</>}
>
  ### You're all set!

  Your app now has a fully functional embedded chat. Explore the detail pages below to add DMs, customize theming, and more.

  ## Next steps

  <CardGroup cols={2}>
    <Card title="Sync your users" icon="users" href="/developer/guides/chat/authentication#sync-your-users">
      Enroll your users on Whop and sync their display name and avatar so they show up correctly in chat.
    </Card>

    <Card title="Chat element" icon="message" href="/developer/guides/chat/chat-element">
      Full props reference, events, and deeplinking for the chat view.
    </Card>

    <Card title="DMs list element" icon="inbox" href="/developer/guides/chat/dms-list-element">
      Display a list of direct message conversations with navigation.
    </Card>

    <Card title="Authentication" icon="key" href="/developer/guides/chat/authentication">
      OAuth setup, manual sign-in, and pre-filling tokens.
    </Card>

    <Card title="Theming & styles" icon="palette" href="/developer/guides/chat/theming-and-styles">
      Customize the appearance of chat elements.
    </Card>
  </CardGroup>
</QuickStart>
