- Select how you'd like to use LLM Gateway, or skip to continue
- with the free plan
+ {isLocalhost
+ ? "You're self-hosting (detected) - explore all your options below, or skip to continue with what you have."
+ : isProPlan
+ ? "You're on the Pro plan. Explore additional options below, or skip to continue building."
+ : "You're currently on the Free plan. Explore upgrade options below, or skip to continue building."}
+
-
- {/* Credits Option */}
-
-
-
-
- Buy Credits
-
-
- Use our managed service with pay-as-you-go credits
-
-
-
-
-
-
-
- Simple pay-as-you-go pricing
-
-
-
- No API key management needed
-
-
-
- Built-in rate limiting and monitoring
-
-
-
- Works with any plan
-
-
-
-
-
-
-
- {/* BYOK Option */}
-
-
-
-
- Bring Your Own Keys
- {!isProPlan && (
-
- Pro Plan Required
-
- )}
-
-
- Use your own API keys for LLM providers
-
-
-
-
);
diff --git a/apps/ui/src/components/onboarding/referral-step.tsx b/apps/ui/src/components/onboarding/referral-step.tsx
index f5fc93d2f..969262ef0 100644
--- a/apps/ui/src/components/onboarding/referral-step.tsx
+++ b/apps/ui/src/components/onboarding/referral-step.tsx
@@ -1,14 +1,8 @@
"use client";
-import { useState } from "react";
+import { useEffect, useRef, useState } from "react";
-import { Button } from "@/lib/components/button";
-import {
- Card,
- CardContent,
- CardHeader,
- CardTitle,
-} from "@/lib/components/card";
+import { Card, CardContent } from "@/lib/components/card";
import { Input } from "@/lib/components/input";
import { Label } from "@/lib/components/label";
import { RadioGroup, RadioGroupItem } from "@/lib/components/radio-group";
@@ -20,6 +14,12 @@ interface ReferralStepProps {
export function ReferralStep({ onComplete }: ReferralStepProps) {
const [selectedSource, setSelectedSource] = useState("");
const [otherDetails, setOtherDetails] = useState("");
+ const onCompleteRef = useRef(onComplete);
+
+ // Keep the ref updated with the latest onComplete function
+ useEffect(() => {
+ onCompleteRef.current = onComplete;
+ }, [onComplete]);
const referralSources = [
{ value: "twitter", label: "X (Formerly Twitter)" },
@@ -30,33 +30,45 @@ export function ReferralStep({ onComplete }: ReferralStepProps) {
{ value: "other", label: "Other (Specify)" },
];
- const handleContinue = () => {
- if (selectedSource) {
- const details = selectedSource === "other" ? otherDetails : undefined;
- onComplete?.(selectedSource, details);
+ // Handle completion when a source is selected or manually triggered
+ useEffect(() => {
+ if (selectedSource && selectedSource !== "other") {
+ // Auto-complete for non-"other" selections after a brief delay
+ const timer = setTimeout(() => {
+ onCompleteRef.current?.(selectedSource);
+ }, 1000); // Increased delay to give users time to change their mind
+ return () => clearTimeout(timer);
}
- };
+ // Return empty cleanup function if condition is not met
+ return () => {};
+ }, [selectedSource]);
- const handleSkip = () => {
- onComplete?.("");
- };
+ // Handle "other" completion when details are provided
+ useEffect(() => {
+ if (selectedSource === "other" && otherDetails.trim()) {
+ const timer = setTimeout(() => {
+ onCompleteRef.current?.(selectedSource, otherDetails);
+ }, 1500); // Longer delay for typing
+ return () => clearTimeout(timer);
+ }
+ // Return empty cleanup function if condition is not met
+ return () => {};
+ }, [selectedSource, otherDetails]);
return (
-
- How did you hear about us?
+
+ How did you find us?
- Help us understand how people discover LLM Gateway (Optional)
+ Help us improve by letting us know how you found us. No pressure - you
+ can skip this step.
+ {selectedSource === "other" && !otherDetails.trim()
+ ? "Please provide details above, or use the Next button to continue."
+ : "Thanks! Moving to the next step automatically..."}
+
+ )}
-
-
-
-
-
);
}
diff --git a/apps/ui/src/components/onboarding/welcome-step.tsx b/apps/ui/src/components/onboarding/welcome-step.tsx
index 732c304c5..11584251c 100644
--- a/apps/ui/src/components/onboarding/welcome-step.tsx
+++ b/apps/ui/src/components/onboarding/welcome-step.tsx
@@ -1,80 +1,314 @@
-import { Rocket } from "lucide-react";
+import { useQueryClient } from "@tanstack/react-query";
+import { Copy, Key, KeyIcon } from "lucide-react";
+import { useRouter } from "next/navigation";
import * as React from "react";
+import { useState, useEffect, useCallback } from "react";
+import { useDefaultProject } from "@/hooks/useDefaultProject";
import { useAuth } from "@/lib/auth-client";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "@/lib/components/card";
+import { Button } from "@/lib/components/button";
import { Step } from "@/lib/components/stepper";
+import { toast } from "@/lib/components/use-toast";
+import { useAppConfig } from "@/lib/config";
+import { useApi } from "@/lib/fetch-client";
export function WelcomeStep() {
const { useSession } = useAuth();
const session = useSession();
const user = session.data?.user;
- const organization = { name: "Your Organization" };
+ const { data: defaultProject } = useDefaultProject();
+ const api = useApi();
+ const queryClient = useQueryClient();
+ const router = useRouter();
+ const [apiKey, setApiKey] = useState(null);
+ const config = useAppConfig();
+ const isLocalhost = !config.hosted;
+
+ const createApiKeyMutation = api.useMutation("post", "/keys/api");
+ const completeOnboarding = api.useMutation(
+ "post",
+ "/user/me/complete-onboarding",
+ );
+
+ // Fetch existing API keys to check if one already exists
+ const { data: existingKeys, refetch: refetchKeys } = api.useQuery(
+ "get",
+ "/keys/api",
+ {
+ params: {
+ query: { projectId: defaultProject?.id || "" },
+ },
+ },
+ {
+ enabled: !!defaultProject?.id && !!user, // Only fetch if user is authenticated
+ staleTime: 0, // Always fetch fresh data
+ refetchOnWindowFocus: true,
+ refetchOnMount: true,
+ },
+ );
+
+ // Refetch API keys when component mounts to get fresh data
+ useEffect(() => {
+ if (defaultProject?.id && user) {
+ refetchKeys();
+ }
+ }, [defaultProject?.id, user, refetchKeys]);
+
+ // Also invalidate cache on mount to ensure fresh data
+ useEffect(() => {
+ if (defaultProject?.id) {
+ const queryKey = api.queryOptions("get", "/keys/api", {
+ params: {
+ query: { projectId: defaultProject.id },
+ },
+ }).queryKey;
+ queryClient.invalidateQueries({ queryKey });
+ }
+ }, [defaultProject?.id, api, queryClient]);
+
+ const createApiKey = useCallback(async () => {
+ // Security: Multiple authentication checks
+ if (!user) {
+ console.error("Unauthorized: No user session");
+ toast({
+ title: "Authentication Error",
+ description: "Please sign in to create API keys.",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ if (!defaultProject?.id) {
+ console.error("Unauthorized: No project access");
+ toast({
+ title: "Access Error",
+ description: "No project found. Please contact support.",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ try {
+ const response = await createApiKeyMutation.mutateAsync({
+ body: {
+ description: "GATEWAY-001",
+ projectId: defaultProject.id,
+ usageLimit: null,
+ },
+ });
+ setApiKey(response.apiKey.token);
+
+ // Invalidate API keys cache to ensure other components see the new key
+ const queryKey = api.queryOptions("get", "/keys/api", {
+ params: {
+ query: { projectId: defaultProject.id },
+ },
+ }).queryKey;
+ queryClient.invalidateQueries({ queryKey });
+ } catch (error) {
+ console.error("Failed to create API key:", error);
+ toast({
+ title: "Error",
+ description:
+ "Failed to create API key. We'll help you create one in the next step.",
+ variant: "destructive",
+ });
+ }
+ }, [user, defaultProject?.id, createApiKeyMutation, api, queryClient]);
+
+ // Auto-create API key on component mount only if no existing keys
+ useEffect(() => {
+ // Security: Only proceed if user is authenticated
+ if (!user) {
+ return;
+ }
+
+ // Don't create keys if we're still loading existing keys data
+ if (existingKeys === undefined) {
+ return;
+ }
+
+ const hasExistingKeys =
+ existingKeys?.apiKeys && existingKeys.apiKeys.length > 0;
+ const shouldCreateKey =
+ defaultProject?.id &&
+ !apiKey &&
+ !createApiKeyMutation.isPending &&
+ !hasExistingKeys;
+
+ if (shouldCreateKey) {
+ createApiKey();
+ }
+ }, [
+ user,
+ defaultProject?.id,
+ apiKey,
+ createApiKeyMutation.isPending,
+ createApiKey,
+ existingKeys,
+ ]);
+
+ const copyToClipboard = () => {
+ if (apiKey) {
+ navigator.clipboard.writeText(apiKey);
+ toast({
+ title: "Copied to clipboard",
+ description: "API key copied to clipboard",
+ });
+ }
+ };
+
+ // Security: Ensure user is authenticated before proceeding
+ if (!user) {
+ return (
+
+
+
Authentication Required
+
+ Please sign in to continue with the onboarding process.
+
+
+
+ );
+ }
return (
-
-
-
Welcome to LLM Gateway!
-
- Let's get you set up with everything you need to start using the
- platform.
+
+ {/* Hero Section */}
+
+
+ Welcome to LLM Gateway
+
+
+ {isLocalhost
+ ? `You're all set up for self-hosting! ${!existingKeys?.apiKeys?.length ? "Let's get you connected to the platform with your first API key." : ""}`
+ : `${existingKeys?.apiKeys?.length ? "You can skip this step and go to the dashboard to manage your API keys." : "Let's get you connected to the platform with your first API key."}`}
-
-
-
-
- Your Project is Ready
-
-
- We've automatically created a project for you to get started.
-
-
-
-
+ You have {existingKeys.apiKeys.length} API key
+ {existingKeys.apiKeys.length > 1 ? "s" : ""} configured
+ and ready to use. You can manage{" "}
+ {existingKeys.apiKeys.length > 1 ? "them" : "it"} in the
+ dashboard.
+
+ {defaultProject && (
+
+ Keys are counted from your default project:{" "}
+
+ {defaultProject.name}
+
+
+ )}
+
+ ) : (
+
+ Setting up your first API key to get started...
+
+ )}
+
+ )}
-
-
- In this onboarding process, we'll help you:
-
-
-
Create your first API key to access the LLM Gateway
-
- Choose between buying credits or bringing your own API keys
-
-
Set up your preferred payment method or provider keys