Skip to content

Commit efef8f3

Browse files
committed
Add extra safeguard for double-invoke
1 parent 3438c23 commit efef8f3

2 files changed

Lines changed: 75 additions & 12 deletions

File tree

‎packages/ui/src/mosaic/features/reverification/__tests__/use-reverification-with-state.test.tsx‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,56 @@ describe('useReverificationWithState', () => {
9595
expect(result.current[1].isActive).toBe(false);
9696
});
9797

98+
it('cancels an overlapping challenge and preserves the active challenge', () => {
99+
const { result } = renderHook(() => useReverificationWithState(fetcher));
100+
const firstComplete = vi.fn();
101+
const firstCancel = vi.fn();
102+
const secondComplete = vi.fn();
103+
const secondCancel = vi.fn();
104+
105+
act(() => {
106+
capturedOnNeeds?.({ complete: firstComplete, cancel: firstCancel, level: 'first_factor' });
107+
capturedOnNeeds?.({ complete: secondComplete, cancel: secondCancel, level: 'multi_factor' });
108+
});
109+
110+
expect(secondCancel).toHaveBeenCalledOnce();
111+
expect(result.current[1]).toMatchObject({ isActive: true, level: 'first_factor' });
112+
113+
act(() => {
114+
const [, state] = result.current;
115+
if (state.isActive) {
116+
state.complete();
117+
}
118+
});
119+
120+
expect(firstComplete).toHaveBeenCalledOnce();
121+
expect(firstCancel).not.toHaveBeenCalled();
122+
expect(secondComplete).not.toHaveBeenCalled();
123+
});
124+
125+
it('allows another challenge after the active challenge settles', () => {
126+
const { result } = renderHook(() => useReverificationWithState(fetcher));
127+
const firstComplete = vi.fn();
128+
const secondCancel = vi.fn();
129+
130+
act(() => {
131+
capturedOnNeeds?.({ complete: firstComplete, cancel: vi.fn(), level: 'first_factor' });
132+
});
133+
act(() => {
134+
const [, state] = result.current;
135+
if (state.isActive) {
136+
state.complete();
137+
}
138+
});
139+
act(() => {
140+
capturedOnNeeds?.({ complete: vi.fn(), cancel: secondCancel, level: 'multi_factor' });
141+
});
142+
143+
expect(firstComplete).toHaveBeenCalledOnce();
144+
expect(secondCancel).not.toHaveBeenCalled();
145+
expect(result.current[1]).toMatchObject({ isActive: true, level: 'multi_factor' });
146+
});
147+
98148
it('cancels when the session changes after reverification opens', async () => {
99149
const { result, rerender } = renderHook(() => useReverificationWithState(fetcher));
100150
const cancel = vi.fn();

‎packages/ui/src/mosaic/features/reverification/use-reverification-with-state.ts‎

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,32 +25,43 @@ export function useReverificationWithState<F extends Fetcher>(
2525
const { session } = useSession();
2626
const [reverificationState, setReverificationState] = useState<ReverificationProps>({ isActive: false });
2727
const openedSessionId = useRef<string | null>(null);
28-
const cancelOnUnmountRef = useRef<(() => void) | undefined>(undefined);
28+
const activeCancelRef = useRef<(() => void) | undefined>(undefined);
2929

3030
const wrapped = useReverification(fetcher, {
3131
...options,
3232
onNeedsReverification: ({ complete, cancel, level }) => {
33+
// If another Reverification is already in progress for this specific
34+
// wrapped action, cancel this new one immediately and keep the old one.
35+
// This is an extra safeguard for something that likely never happens.
36+
if (activeCancelRef.current) {
37+
cancel();
38+
return;
39+
}
40+
3341
openedSessionId.current = session?.id ?? null;
42+
activeCancelRef.current = cancel;
43+
44+
const settle = (callback: () => void) => {
45+
if (activeCancelRef.current !== cancel) {
46+
return;
47+
}
48+
activeCancelRef.current = undefined;
49+
setReverificationState({ isActive: false });
50+
callback();
51+
};
52+
3453
setReverificationState({
3554
isActive: true,
3655
level,
37-
complete: () => {
38-
setReverificationState({ isActive: false });
39-
complete();
40-
},
41-
cancel: () => {
42-
setReverificationState({ isActive: false });
43-
cancel();
44-
},
56+
complete: () => settle(complete),
57+
cancel: () => settle(cancel),
4558
});
4659
},
4760
});
4861

4962
// Cancel if the session changes mid-flight
5063
const { isActive, cancel } = reverificationState;
5164
useEffect(() => {
52-
cancelOnUnmountRef.current = isActive ? cancel : undefined;
53-
5465
if (!isActive) {
5566
openedSessionId.current = null;
5667
return;
@@ -74,7 +85,9 @@ export function useReverificationWithState<F extends Fetcher>(
7485

7586
useEffect(() => {
7687
return () => {
77-
cancelOnUnmountRef.current?.();
88+
const cancel = activeCancelRef.current;
89+
activeCancelRef.current = undefined;
90+
cancel?.();
7891
};
7992
}, []);
8093

0 commit comments

Comments
 (0)