11import { useEffect , useMemo , useState } from 'react' ;
2- import { type EventData , EVENTS , Joyride , STATUS } from 'react-joyride' ;
2+ import { type Controls , type EventData , EVENTS , Joyride , STATUS } from 'react-joyride' ;
33import { useNavigate } from 'react-router-dom' ;
44
5+ import { getStepGate } from './interactiveGates' ;
56import { createWalkthroughSteps } from './walkthroughSteps' ;
67import WalkthroughTooltip from './WalkthroughTooltip' ;
78
89// ── localStorage keys ──────────────────────────────────────────────────────
910
1011const WALKTHROUGH_KEY = 'openhuman:walkthrough_completed' ;
1112const WALKTHROUGH_PENDING_KEY = 'openhuman:walkthrough_pending' ;
13+ export const WALKTHROUGH_STEP_KEY = 'openhuman:walkthrough_step' ;
1214
1315/**
1416 * Returns `true` when the walkthrough should be shown. This is true when:
@@ -58,6 +60,7 @@ export function markWalkthroughComplete(): void {
5860 try {
5961 localStorage . setItem ( WALKTHROUGH_KEY , 'true' ) ;
6062 localStorage . removeItem ( WALKTHROUGH_PENDING_KEY ) ;
63+ localStorage . removeItem ( WALKTHROUGH_STEP_KEY ) ;
6164 console . debug ( '[walkthrough] marked as complete' ) ;
6265 } catch ( e ) {
6366 console . warn ( '[walkthrough] could not mark walkthrough complete in localStorage' , e ) ;
@@ -75,6 +78,7 @@ export function markWalkthroughComplete(): void {
7578export function resetWalkthrough ( ) : void {
7679 try {
7780 localStorage . removeItem ( WALKTHROUGH_KEY ) ;
81+ localStorage . removeItem ( WALKTHROUGH_STEP_KEY ) ;
7882 localStorage . setItem ( WALKTHROUGH_PENDING_KEY , 'true' ) ;
7983 console . debug ( '[walkthrough] reset — pending flag set, completed flag removed' ) ;
8084 } catch ( e ) {
@@ -83,6 +87,33 @@ export function resetWalkthrough(): void {
8387 window . dispatchEvent ( new CustomEvent ( 'walkthrough:restart' ) ) ;
8488}
8589
90+ // ── Step persistence helpers ───────────────────────────────────────────────
91+
92+ function getSavedStepIndex ( ) : number {
93+ try {
94+ const saved = localStorage . getItem ( WALKTHROUGH_STEP_KEY ) ;
95+ return saved ? Math . max ( 0 , parseInt ( saved , 10 ) || 0 ) : 0 ;
96+ } catch {
97+ return 0 ;
98+ }
99+ }
100+
101+ function saveStepIndex ( index : number ) : void {
102+ try {
103+ localStorage . setItem ( WALKTHROUGH_STEP_KEY , String ( index ) ) ;
104+ } catch ( e ) {
105+ console . warn ( '[walkthrough] could not save step index' , e ) ;
106+ }
107+ }
108+
109+ function clearStepIndex ( ) : void {
110+ try {
111+ localStorage . removeItem ( WALKTHROUGH_STEP_KEY ) ;
112+ } catch ( e ) {
113+ console . warn ( '[walkthrough] could not clear step index' , e ) ;
114+ }
115+ }
116+
86117// ── Component ──────────────────────────────────────────────────────────────
87118
88119/**
@@ -103,6 +134,9 @@ const AppWalkthrough = ({ onboarded = false }: { onboarded?: boolean }) => {
103134 // Using a lazy initializer keeps this stable across re-renders.
104135 const [ run , setRun ] = useState < boolean > ( ( ) => isWalkthroughPending ( onboarded ) ) ;
105136
137+ // Track the current step index for controlled mode — enables resume support.
138+ const [ stepIndex , setStepIndex ] = useState < number > ( ( ) => getSavedStepIndex ( ) ) ;
139+
106140 // Memoize steps so they are only recreated when `navigate` identity changes.
107141 const steps = useMemo ( ( ) => createWalkthroughSteps ( navigate ) , [ navigate ] ) ;
108142
@@ -111,6 +145,8 @@ const AppWalkthrough = ({ onboarded = false }: { onboarded?: boolean }) => {
111145 useEffect ( ( ) => {
112146 const handleRestart = ( ) => {
113147 console . debug ( '[walkthrough] restart event received — restarting tour' ) ;
148+ clearStepIndex ( ) ;
149+ setStepIndex ( 0 ) ;
114150 setRun ( true ) ;
115151 } ;
116152 window . addEventListener ( 'walkthrough:restart' , handleRestart ) ;
@@ -119,14 +155,33 @@ const AppWalkthrough = ({ onboarded = false }: { onboarded?: boolean }) => {
119155 } ;
120156 } , [ ] ) ;
121157
122- const handleEvent = ( data : EventData ) => {
158+ const handleEvent = ( data : EventData , controls : Controls ) => {
123159 const { type, status } = data ;
124160 console . debug ( '[walkthrough] event' , { type, status, index : data . index } ) ;
125161
162+ // STEP_BEFORE: auto-skip gated steps whose gate is already satisfied.
163+ if ( type === EVENTS . STEP_BEFORE ) {
164+ const gate = getStepGate ( steps [ data . index ] ) ;
165+ if ( gate && gate . isComplete ( ) ) {
166+ console . debug ( '[walkthrough] gate already complete, auto-skipping step' , data . index ) ;
167+ // Use setTimeout to avoid calling controls.next() during the event handler.
168+ setTimeout ( ( ) => controls . next ( ) , 0 ) ;
169+ return ;
170+ }
171+ }
172+
173+ // STEP_AFTER: persist the next step index so the tour can resume.
174+ if ( type === EVENTS . STEP_AFTER ) {
175+ const nextIndex = data . index + 1 ;
176+ setStepIndex ( nextIndex ) ;
177+ saveStepIndex ( nextIndex ) ;
178+ }
179+
126180 // TOUR_END fires when the tour finishes or is skipped.
127181 if ( type === EVENTS . TOUR_END ) {
128182 if ( status === STATUS . FINISHED || status === STATUS . SKIPPED ) {
129183 markWalkthroughComplete ( ) ;
184+ clearStepIndex ( ) ;
130185 setRun ( false ) ;
131186 }
132187 }
@@ -139,6 +194,7 @@ const AppWalkthrough = ({ onboarded = false }: { onboarded?: boolean }) => {
139194 < Joyride
140195 steps = { steps }
141196 run = { run }
197+ stepIndex = { stepIndex }
142198 continuous = { true }
143199 tooltipComponent = { WalkthroughTooltip }
144200 onEvent = { handleEvent }
0 commit comments