Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions apps/dashboard/app/new/components/onboarding-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,20 @@ import { type OnboardingStep, OnboardingWizard } from "./onboarding-wizard";
export function OnboardingContent() {
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const workspaceStep = useWorkspaceStep();
const keyCreationStep = useKeyCreationStep();

const handleStepChange = (newStepIndex: number) => {
if (newStepIndex >= 0 && newStepIndex < steps.length) {
setCurrentStepIndex(newStepIndex);
}
};
const workspaceStep = useWorkspaceStep({
advance: () => {
handleStepChange(currentStepIndex + 1);
},
});
const keyCreationStep = useKeyCreationStep({
advance: () => handleStepChange(currentStepIndex + 1),
});

const steps: OnboardingStep[] = [
workspaceStep,
Expand All @@ -29,17 +41,14 @@ export function OnboardingContent() {
buttonText: "Continue to dashboard",
onStepNext: () => {
setIsConfirmOpen(true);
return true;
},
onStepSkip: () => {
setIsConfirmOpen(true);
},
},
];

const handleStepChange = (newStepIndex: number) => {
setCurrentStepIndex(newStepIndex);
};

const currentStepInfo = stepInfos[currentStepIndex];

return (
Expand Down Expand Up @@ -69,7 +78,11 @@ export function OnboardingContent() {
<div className="mt-10" />
{/* Form part */}
<div className="flex-1 min-h-0 overflow-y-auto overscroll-y-contain pb-[calc(6rem+env(safe-area-inset-bottom))]">
<OnboardingWizard steps={steps} onStepChange={handleStepChange} />
<OnboardingWizard
steps={steps}
currentStepIndex={currentStepIndex}
setCurrentStepIndex={handleStepChange}
/>
</div>
</div>
<div className="absolute bottom-4 left-4">
Expand Down
5 changes: 2 additions & 3 deletions apps/dashboard/app/new/components/onboarding-fallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,11 @@ export function OnboardingFallback() {
requiredFieldCount: 1,
buttonText: "Continue",
description: "Set up your workspace to get started",
onStepNext: () => {},
onStepBack: () => {},
},
]}
onComplete={() => {}}
onStepChange={() => {}}
currentStepIndex={0}
setCurrentStepIndex={() => {}}
/>
</div>
</div>
Expand Down
50 changes: 8 additions & 42 deletions apps/dashboard/app/new/components/onboarding-wizard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";
import { ChevronLeft, ChevronRight } from "@unkey/icons";
import { Button, CircleProgress, Separator } from "@unkey/ui";
import { useEffect, useRef, useState } from "react";

export type OnboardingStep = {
/** Display name of the step shown in the navigation */
Expand Down Expand Up @@ -40,16 +39,16 @@ export type OnboardingStep = {
export type OnboardingWizardProps = {
/** Array of steps to display in the wizard. Must contain at least one step. */
steps: OnboardingStep[];
/** Callback fired when the wizard is completed (user clicks continue on last step) */
onComplete?: () => void;
/** Callback fired whenever the current step changes */
onStepChange?: (stepIndex: number) => void;
currentStepIndex: number;
setCurrentStepIndex: (index: number) => void;
};

export const OnboardingWizard = ({ steps, onComplete, onStepChange }: OnboardingWizardProps) => {
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const previousLoadingRef = useRef<boolean>(false);

export const OnboardingWizard = ({
steps,
currentStepIndex,
setCurrentStepIndex,
}: OnboardingWizardProps) => {
if (steps.length === 0) {
throw new Error("OnboardingWizard requires at least one step");
}
Expand All @@ -59,31 +58,6 @@ export const OnboardingWizard = ({ steps, onComplete, onStepChange }: Onboarding
const isLastStep = currentStepIndex === steps.length - 1;
const isLoading = currentStep.isLoading || false;

// Auto-advance when loading ends
useEffect(() => {
if (previousLoadingRef.current && !isLoading) {
// Loading just ended, advance to next step
if (isLastStep) {
onComplete?.();
} else {
setCurrentStepIndex(currentStepIndex + 1);
}
}
previousLoadingRef.current = isLoading;
}, [isLoading, isLastStep, currentStepIndex, onComplete]);

useEffect(() => {
onStepChange?.(currentStepIndex);
}, [currentStepIndex, onStepChange]);

const advanceStep = () => {
if (isLastStep) {
onComplete?.();
} else {
setCurrentStepIndex(currentStepIndex + 1);
}
};

const handleBack = () => {
if (!isFirstStep && !isLoading) {
currentStep.onStepBack?.(currentStepIndex);
Expand All @@ -96,15 +70,7 @@ export const OnboardingWizard = ({ steps, onComplete, onStepChange }: Onboarding
return;
}

// If no callback provided, advance immediately
if (!currentStep.onStepNext) {
advanceStep();
return;
}

// Trigger callback, step should handle its own advancement via loading state
// or by calling the wizard's advance function passed to the callback
currentStep.onStepNext(currentStepIndex);
currentStep.onStepNext?.(currentStepIndex);
};

const handleSkip = () => {
Expand Down
16 changes: 10 additions & 6 deletions apps/dashboard/app/new/hooks/use-key-creation-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ const extendedFormSchema = formSchema.and(
.max(50, "API name must not exceed 50 characters"),
}),
);

export const useKeyCreationStep = (): OnboardingStep => {
type Props = {
// Move to the next step
advance: () => void;
};
export const useKeyCreationStep = (props: Props): OnboardingStep => {
const [apiCreated, setApiCreated] = useState(false);
const formRef = useRef<HTMLFormElement>(null);
const router = useRouter();
Expand Down Expand Up @@ -94,6 +97,7 @@ export const useKeyCreationStep = (): OnboardingStep => {
};

await createApiAndKey.mutateAsync(submitData);
props.advance();
} catch (error) {
console.error("Submit error:", error);
}
Expand Down Expand Up @@ -228,12 +232,12 @@ export const useKeyCreationStep = (): OnboardingStep => {
router.push("/apis");
},
onStepNext: apiCreated
? undefined
? () => true
: () => {
if (isLoading) {
return;
if (!isLoading) {
formRef.current?.requestSubmit();
}
formRef.current?.requestSubmit();
return false;
},
isLoading,
};
Expand Down
61 changes: 23 additions & 38 deletions apps/dashboard/app/new/hooks/use-workspace-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ const workspaceSchema = z.object({

type WorkspaceFormData = z.infer<typeof workspaceSchema>;

export const useWorkspaceStep = (): OnboardingStep => {
type Props = {
// Move to the next step
advance: () => void;
};

export const useWorkspaceStep = (props: Props): OnboardingStep => {
const [slugManuallyEdited, setSlugManuallyEdited] = useState(false);
const [workspaceCreated, setWorkspaceCreated] = useState(false);
const formRef = useRef<HTMLFormElement>(null);
Expand Down Expand Up @@ -67,9 +72,10 @@ export const useWorkspaceStep = (): OnboardingStep => {
});

const createWorkspace = trpc.workspace.create.useMutation({
onSuccess: async ({ organizationId }) => {
onSuccess: async ({ orgId }) => {
setWorkspaceCreated(true);
switchOrgMutation.mutate(organizationId);
await switchOrgMutation.mutateAsync(orgId);
props.advance();
},
onError: (error) => {
if (error.data?.code === "METHOD_NOT_SUPPORTED") {
Expand All @@ -93,6 +99,8 @@ export const useWorkspaceStep = (): OnboardingStep => {
</div>
),
});
} else if (error.data?.code === "CONFLICT") {
form.setError("slug", { message: error.message }, { shouldFocus: true });
} else {
toast.error(`Failed to create workspace: ${error.message}`);
}
Expand Down Expand Up @@ -125,32 +133,6 @@ export const useWorkspaceStep = (): OnboardingStep => {
body: (
<form ref={formRef} onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex flex-col">
{/* <div className="flex flex-row py-1.5 gap-[18px]"> */}
{/* <div className="bg-gradient-to-br from-info-5 to-blue-9 size-20 border rounded-2xl border-grayA-6" /> */}
{/* <div className="flex flex-col gap-2"> */}
{/* <div className="text-gray-11 text-[13px] leading-6">Company workspace logo</div> */}
{/* <div className="flex items-center gap-2"> */}
{/* <Button variant="outline" className="w-fit"> */}
{/* <div className="gap-2 flex items-center text-[13px] leading-4 font-medium"> */}
{/* <Refresh3 className="text-gray-12 !w-3 !h-3 flex-shrink-0" size="sm-regular" /> */}
{/* Upload */}
{/* </div> */}
{/* </Button> */}
{/* <Button variant="outline" className="w-fit"> */}
{/* <div className="gap-2 flex items-center text-[13px] leading-4 font-medium"> */}
{/* <Refresh3 className="text-gray-12 !w-3 !h-3 flex-shrink-0" size="sm-regular" /> */}
{/* Gradient */}
{/* </div> */}
{/* </Button> */}
{/* <Trash size="md-regular" className="text-gray-8 ml-[10px]" /> */}
{/* </div> */}
{/* <div className="text-gray-9 text-xs leading-6"> */}
{/* .png, .jpg, or .svg up to 10MB, and 480×480px */}
{/* </div> */}
{/* </div> */}
{/* </div> */}
{/* Use this 'pt-7' version when implementing profile photo and slug based onboarding*/}
{/* <div className="space-y-4 pt-7"> */}
<div className="space-y-4 p-1">
<FormInput
{...form.register("workspaceName")}
Expand Down Expand Up @@ -180,6 +162,9 @@ export const useWorkspaceStep = (): OnboardingStep => {
prefix="app.unkey.com/"
maxLength={64}
onChange={(evt) => {
// If we don't clear the manually set error, it will persist even if the user clears
// or changes the input
form.clearErrors("slug");
const v = evt.currentTarget.value;
setSlugManuallyEdited(v.length > 0);
}}
Expand All @@ -195,15 +180,15 @@ export const useWorkspaceStep = (): OnboardingStep => {
description: workspaceCreated
? "Workspace created successfully, continue to next step"
: "Set up your workspace to get started",
onStepNext: workspaceCreated
? undefined
: () => {
if (isLoading) {
return;
}

formRef.current?.requestSubmit();
},
onStepNext: () => {
if (workspaceCreated) {
props.advance();
return;
}
if (!isLoading) {
formRef.current?.requestSubmit();
}
},
onStepBack: () => {
console.info("Going back from workspace step");
},
Expand Down
Loading