From 0711d362abd46ed290de33f3e96c569b23e3ae58 Mon Sep 17 00:00:00 2001
From: DavisVT
Date: Tue, 2 Jun 2026 14:23:09 +0100
Subject: [PATCH] Implement interactive onboarding flow for new users
---
frontend/app/components/OnboardingWizard.tsx | 115 ++++++++++++++++
frontend/app/context/OnboardingContext.tsx | 128 ++++++++++++++++++
frontend/app/dashboard/DashboardProviders.tsx | 28 +++-
.../app/dashboard/settings/SettingsClient.tsx | 21 +++
4 files changed, 287 insertions(+), 5 deletions(-)
create mode 100644 frontend/app/components/OnboardingWizard.tsx
create mode 100644 frontend/app/context/OnboardingContext.tsx
diff --git a/frontend/app/components/OnboardingWizard.tsx b/frontend/app/components/OnboardingWizard.tsx
new file mode 100644
index 00000000..4162bcfc
--- /dev/null
+++ b/frontend/app/components/OnboardingWizard.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import React from "react";
+import { useOnboarding } from "../context/OnboardingContext";
+import { ArrowLeft, ArrowRight, CheckCircle2, Sparkles, X } from "lucide-react";
+import { Button } from "./ui/Button";
+
+const ONBOARDING_STEPS = [
+ {
+ title: "Welcome to Nestera!",
+ description: "Your gateway to smart savings and decentralized finance.",
+ icon: ,
+ },
+ {
+ title: "Connect Your Wallet",
+ description: "Link your crypto wallet to start using all features.",
+ icon: ,
+ },
+ {
+ title: "Explore the Dashboard",
+ description: "Get familiar with your portfolio, goals, and savings pools.",
+ icon: ,
+ },
+ {
+ title: "Create Your First Goal",
+ description: "Set a savings target and start growing your funds.",
+ icon: ,
+ },
+ {
+ title: "Make Your First Deposit",
+ description: "Fund your goal and watch your savings grow with yield.",
+ icon: ,
+ },
+ {
+ title: "You're All Set!",
+ description: "Congratulations! You're ready to start your savings journey.",
+ icon: ,
+ },
+];
+
+export function OnboardingWizard() {
+ const {
+ isOnboardingActive,
+ currentStep,
+ nextStep,
+ prevStep,
+ completeOnboarding,
+ skipOnboarding,
+ } = useOnboarding();
+
+ if (!isOnboardingActive) return null;
+
+ const step = ONBOARDING_STEPS[currentStep];
+ const isLastStep = currentStep === ONBOARDING_STEPS.length - 1;
+ const isFirstStep = currentStep === 0;
+
+ return (
+
+
+
+
+ Step {currentStep + 1} of {ONBOARDING_STEPS.length}
+
+
+
+
+
+
+
+
+ {step.icon}
+
+
{step.title}
+
{step.description}
+
+
+
+ {!isFirstStep && (
+
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend/app/context/OnboardingContext.tsx b/frontend/app/context/OnboardingContext.tsx
new file mode 100644
index 00000000..7dca4fad
--- /dev/null
+++ b/frontend/app/context/OnboardingContext.tsx
@@ -0,0 +1,128 @@
+"use client";
+
+import React, {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
+
+const ONBOARDING_STORAGE_KEY = "nestera-onboarding-completed";
+
+interface OnboardingContextValue {
+ isOnboardingCompleted: boolean;
+ isOnboardingActive: boolean;
+ currentStep: number;
+ startOnboarding: () => void;
+ completeOnboarding: () => void;
+ resetOnboarding: () => void;
+ nextStep: () => void;
+ prevStep: () => void;
+ goToStep: (step: number) => void;
+ skipOnboarding: () => void;
+}
+
+const OnboardingContext = createContext(null);
+
+function readInitialOnboardingState(): boolean {
+ if (typeof window === "undefined") return false;
+ try {
+ return window.localStorage.getItem(ONBOARDING_STORAGE_KEY) === "true";
+ } catch {
+ return false;
+ }
+}
+
+export function OnboardingProvider({ children }: { children: React.ReactNode }) {
+ const [isOnboardingCompleted, setIsOnboardingCompleted] = useState(() =>
+ readInitialOnboardingState()
+ );
+ const [isOnboardingActive, setIsOnboardingActive] = useState(false);
+ const [currentStep, setCurrentStep] = useState(0);
+
+ useEffect(() => {
+ if (!isOnboardingCompleted) {
+ setIsOnboardingActive(true);
+ }
+ }, [isOnboardingCompleted]);
+
+ const startOnboarding = useCallback(() => {
+ setCurrentStep(0);
+ setIsOnboardingActive(true);
+ }, []);
+
+ const completeOnboarding = useCallback(() => {
+ setIsOnboardingActive(false);
+ setIsOnboardingCompleted(true);
+ try {
+ window.localStorage.setItem(ONBOARDING_STORAGE_KEY, "true");
+ } catch {}
+ }, []);
+
+ const resetOnboarding = useCallback(() => {
+ setIsOnboardingCompleted(false);
+ try {
+ window.localStorage.removeItem(ONBOARDING_STORAGE_KEY);
+ } catch {}
+ startOnboarding();
+ }, [startOnboarding]);
+
+ const nextStep = useCallback(() => {
+ setCurrentStep((prev) => prev + 1);
+ }, []);
+
+ const prevStep = useCallback(() => {
+ setCurrentStep((prev) => Math.max(0, prev - 1));
+ }, []);
+
+ const goToStep = useCallback((step: number) => {
+ setCurrentStep(step);
+ }, []);
+
+ const skipOnboarding = useCallback(() => {
+ completeOnboarding();
+ }, [completeOnboarding]);
+
+ const value = useMemo(
+ () => ({
+ isOnboardingCompleted,
+ isOnboardingActive,
+ currentStep,
+ startOnboarding,
+ completeOnboarding,
+ resetOnboarding,
+ nextStep,
+ prevStep,
+ goToStep,
+ skipOnboarding,
+ }),
+ [
+ isOnboardingCompleted,
+ isOnboardingActive,
+ currentStep,
+ startOnboarding,
+ completeOnboarding,
+ resetOnboarding,
+ nextStep,
+ prevStep,
+ goToStep,
+ skipOnboarding,
+ ]
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useOnboarding() {
+ const context = useContext(OnboardingContext);
+ if (!context) {
+ throw new Error("useOnboarding must be used within OnboardingProvider");
+ }
+ return context;
+}
diff --git a/frontend/app/dashboard/DashboardProviders.tsx b/frontend/app/dashboard/DashboardProviders.tsx
index 94023b39..acfdf706 100644
--- a/frontend/app/dashboard/DashboardProviders.tsx
+++ b/frontend/app/dashboard/DashboardProviders.tsx
@@ -2,13 +2,12 @@
import React from "react";
import { FeatureFlagProvider } from "../context/FeatureFlagContext";
+import { WalletProvider } from "../context/WalletContext";
+import { OnboardingProvider } from "../context/OnboardingContext";
+import { OnboardingWizard } from "../components/OnboardingWizard";
import { useWallet } from "../context/WalletContext";
-/**
- * Client-side providers for the dashboard.
- * Bridges server layout → client context.
- */
-export default function DashboardProviders({
+function InnerDashboardProviders({
children,
}: {
children: React.ReactNode;
@@ -18,6 +17,25 @@ export default function DashboardProviders({
return (
{children}
+
);
}
+
+/**
+ * Client-side providers for the dashboard.
+ * Bridges server layout → client context.
+ */
+export default function DashboardProviders({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/frontend/app/dashboard/settings/SettingsClient.tsx b/frontend/app/dashboard/settings/SettingsClient.tsx
index b996349d..8af79b58 100644
--- a/frontend/app/dashboard/settings/SettingsClient.tsx
+++ b/frontend/app/dashboard/settings/SettingsClient.tsx
@@ -4,6 +4,7 @@ import React from "react";
import { useForm } from "react-hook-form";
import { Settings } from "lucide-react";
import { useTranslations } from "next-intl";
+import { useOnboarding } from "../../context/OnboardingContext";
type Prefs = {
emailNotifications?: boolean;
@@ -16,6 +17,7 @@ type Prefs = {
export default function SettingsClient() {
const t = useTranslations("Settings");
+ const { resetOnboarding } = useOnboarding();
const [optimisticPrefs, setOptimisticPrefs] = React.useState({
emailNotifications: false,
inAppNotifications: false,
@@ -211,6 +213,25 @@ export default function SettingsClient() {
)}
+
+
+
Tutorial
+
+
+
Show onboarding tutorial again
+
+ Replay the interactive guide to Nestera
+
+
+
+
+
);