diff --git a/package.json b/package.json
index 7e788f6..9f03114 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
"@stellar/freighter-api": "^2.0.0",
"next": "^14.2.0",
"react": "^18.3.0",
- "react-dom": "^18.3.0"
+ "react-dom": "^18.3.0",
+ "react-joyride": "^2.9.3"
},
"devDependencies": {
"@types/node": "^20.0.0",
diff --git a/src/app/providers.tsx b/src/app/providers.tsx
index 98f8bf5..c91856f 100644
--- a/src/app/providers.tsx
+++ b/src/app/providers.tsx
@@ -2,6 +2,7 @@
import React, { createContext, useContext, useState, useCallback, useEffect } from "react";
import { connectWallet, getPublicKey, isFreighterInstalled } from "@/lib/wallet";
+import { OnboardingTour } from "@/components/OnboardingTour";
interface WalletContextType {
address: string | null;
@@ -54,6 +55,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
disconnect,
}}
>
+
{children}
);
diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx
new file mode 100644
index 0000000..81a439e
--- /dev/null
+++ b/src/components/OnboardingTour.tsx
@@ -0,0 +1,96 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import Joyride, { CallBackProps, STATUS, Step } from "react-joyride";
+
+export const OnboardingTour = () => {
+ const [run, setRun] = useState(false);
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ // Check local storage to see if tour was already completed
+ const hasSeenTour = localStorage.getItem("sorosave_tour_completed");
+ if (!hasSeenTour) {
+ setRun(true);
+ }
+
+ // Listen for custom event to trigger replay
+ const handleReplay = () => {
+ localStorage.removeItem("sorosave_tour_completed");
+ setRun(true);
+ };
+
+ window.addEventListener("replay_onboarding_tour", handleReplay);
+ return () => {
+ window.removeEventListener("replay_onboarding_tour", handleReplay);
+ };
+ }, []);
+
+ const steps: Step[] = [
+ {
+ target: "body",
+ content: "Welcome to Sorosave! Let's take a quick tour of our platform.",
+ placement: "center",
+ disableBeacon: true,
+ },
+ {
+ target: ".tour-wallet-connect",
+ content: "First, connect your wallet to get started.",
+ placement: "bottom",
+ },
+ {
+ target: ".tour-browse-groups",
+ content: "Browse available saving groups to find one that fits your goals.",
+ placement: "bottom",
+ },
+ {
+ target: ".tour-join-group",
+ content: "Join a group to participate in decentralized savings.",
+ placement: "bottom",
+ },
+ {
+ target: ".tour-contribute",
+ content: "Finally, make regular contributions and watch your savings grow!",
+ placement: "top",
+ }
+ ];
+
+ const handleJoyrideCallback = (data: CallBackProps) => {
+ const { status } = data;
+ const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED];
+
+ // If the tour is finished or the user skips it
+ if (finishedStatuses.includes(status)) {
+ setRun(false);
+ localStorage.setItem("sorosave_tour_completed", "true");
+ }
+ };
+
+ if (!isMounted) return null;
+
+ return (
+
+ );
+};
+
+// Expose a helper to trigger the tour manually from Settings/Help menus
+export const triggerTourReplay = () => {
+ if (typeof window !== "undefined") {
+ window.dispatchEvent(new Event("replay_onboarding_tour"));
+ }
+};