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
2 changes: 1 addition & 1 deletion src/components/onboarding/OnboardingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useOnboarding, OnboardingStep } from "@/hooks/useOnboarding";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { ArrowRight, Check, Upload, Settings, Sparkles } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";

import Link from "next/link";

const STEPS_CONFIG: Record<OnboardingStep, { title: string; description: string; icon: React.ReactNode }> = {
Expand Down
117 changes: 117 additions & 0 deletions src/hooks/useOnboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// src/hooks/useOnboarding.ts
"use client";

import { useState, useEffect, useCallback } from "react";

const ONBOARDING_KEY = "tradia_onboarding_complete";
const ONBOARDING_STEP_KEY = "tradia_onboarding_step";
Comment on lines +6 to +7
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope onboarding keys per signed-in user

Because these localStorage keys are global to the browser, completing or skipping onboarding as one signed-in user suppresses it for every later user on the same device/browser profile. I checked the dashboard uses NextAuth sessions, but this hook never includes the session/user id in the key or stores state server-side, so a fresh account can land on /dashboard and have onboarding hidden solely because a previous account set tradia_onboarding_complete=true.

Useful? React with 👍 / 👎.


export type OnboardingStep =
| "welcome"
| "create-account"
| "import-trades"
| "setup-preferences"
| "complete";

interface OnboardingState {
isComplete: boolean;
currentStep: OnboardingStep;
hasAccount: boolean;
hasTrades: boolean;
hasPreferences: boolean;
}

export function useOnboarding() {
const [state, setState] = useState<OnboardingState>({
isComplete: false,
currentStep: "welcome",
hasAccount: false,
hasTrades: false,
hasPreferences: false,
Comment on lines +28 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive onboarding progress from created data

For users who follow the primary CTAs, these flags never get updated from the actual account/trade state, so the onboarding flow gets stuck. I checked that OnboardingModal is only mounted on the dashboard page, while the Add Account and Import Trades flows redirect to /dashboard/trade-history after success; when the user later returns to /dashboard, hasAccount/hasTrades are reinitialized to false, so the modal still shows “Add Trading Account”/“Import Trades” instead of allowing them to continue. Please initialize or subscribe these flags from AccountContext/TradeContext (or persist completion when those flows succeed) rather than keeping them as local-only booleans.

Useful? React with 👍 / 👎.

Comment on lines +28 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive onboarding progress from created data

For users who follow the primary CTAs, these flags never get updated from the actual account/trade/settings state, so the onboarding flow gets stuck. I checked that OnboardingModal is only mounted on the dashboard page, while the Add Account and Import Trades flows redirect to /dashboard/trade-history after success; when the user later returns to /dashboard, hasAccount/hasTrades/hasPreferences are reinitialized to false, so the modal still shows the same setup CTA instead of allowing them to continue. Please initialize or subscribe these flags from the relevant contexts (or persist completion when those flows succeed) rather than keeping them as local-only booleans.

Useful? React with 👍 / 👎.

});
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
if (typeof window === "undefined") {
setIsLoading(false);
return;
}
const stored = localStorage.getItem(ONBOARDING_KEY);
const step = localStorage.getItem(ONBOARDING_STEP_KEY) as OnboardingStep | null;
Comment on lines +39 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate persisted onboarding steps

If tradia_onboarding_step contains anything outside the OnboardingStep union (for example from stale/corrupt localStorage or a browser extension/user edit), this cast accepts it and OnboardingModal then indexes STEPS_CONFIG[currentStep], making config undefined and crashing when rendering config.icon. Please guard the stored value against the allowed step list before putting it in state, and fall back to welcome for invalid values.

Useful? React with 👍 / 👎.


if (stored === "true") {
setState(s => ({ ...s, isComplete: true, currentStep: "complete" }));
} else {
setState(s => ({ ...s, currentStep: step || "welcome" }));
}
setIsLoading(false);
}, []);

const completeStep = useCallback((step: OnboardingStep) => {
let nextStep: OnboardingStep = "welcome";

switch (step) {
case "welcome":
nextStep = "create-account";
break;
case "create-account":
nextStep = "import-trades";
setState(s => ({ ...s, hasAccount: true }));
break;
case "import-trades":
nextStep = "setup-preferences";
setState(s => ({ ...s, hasTrades: true }));
break;
case "setup-preferences":
nextStep = "complete";
setState(s => ({ ...s, hasPreferences: true }));
break;
case "complete":
return;
}

if (typeof window !== "undefined") {
localStorage.setItem(ONBOARDING_STEP_KEY, nextStep);
}
setState(s => ({ ...s, currentStep: nextStep }));
}, []);

const skipOnboarding = useCallback(() => {
if (typeof window !== "undefined") {
localStorage.setItem(ONBOARDING_KEY, "true");
localStorage.removeItem(ONBOARDING_STEP_KEY);
}
setState(s => ({ ...s, isComplete: true, currentStep: "complete" }));
}, []);

const completeOnboarding = useCallback(() => {
if (typeof window !== "undefined") {
localStorage.setItem(ONBOARDING_KEY, "true");
localStorage.removeItem(ONBOARDING_STEP_KEY);
}
setState(s => ({ ...s, isComplete: true, currentStep: "complete" }));
}, []);

const resetOnboarding = useCallback(() => {
if (typeof window !== "undefined") {
localStorage.removeItem(ONBOARDING_KEY);
localStorage.removeItem(ONBOARDING_STEP_KEY);
}
setState({
isComplete: false,
currentStep: "welcome",
hasAccount: false,
hasTrades: false,
hasPreferences: false,
});
}, []);

return {
...state,
isLoading,
completeStep,
skipOnboarding,
completeOnboarding,
resetOnboarding,
};
}
Loading