Skip to content
Open
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,6 +55,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
disconnect,
}}
>
<OnboardingTour />
{children}
</WalletContext.Provider>
);
Expand Down
96 changes: 96 additions & 0 deletions src/components/OnboardingTour.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Joyride
steps={steps}
run={run}
continuous
scrollToFirstStep
showProgress
showSkipButton
callback={handleJoyrideCallback}
styles={{
options: {
primaryColor: "#0f766e",
zIndex: 10000,
},
}}
/>
);
};

// 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"));
}
};