Improve onboarding flow

This commit is contained in:
Urtzi Alfaro
2026-01-04 21:37:44 +01:00
parent 47ccea4900
commit 429e724a2c
13 changed files with 1052 additions and 213 deletions

View File

@@ -2,8 +2,9 @@
* Onboarding React Query hooks
*/
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { useCallback, useRef } from 'react';
import { onboardingService } from '../services/onboarding';
import { UserProgress, UpdateStepRequest } from '../types/onboarding';
import { UserProgress, UpdateStepRequest, StepDraftResponse } from '../types/onboarding';
import { ApiError } from '../client';
// Query Keys
@@ -12,6 +13,7 @@ export const onboardingKeys = {
progress: (userId: string) => [...onboardingKeys.all, 'progress', userId] as const,
steps: () => [...onboardingKeys.all, 'steps'] as const,
stepDetail: (stepName: string) => [...onboardingKeys.steps(), stepName] as const,
stepDraft: (stepName: string) => [...onboardingKeys.all, 'draft', stepName] as const,
} as const;
// Queries
@@ -122,8 +124,6 @@ export const useMarkStepCompleted = (
// Invalidate queries on error to ensure we get fresh data
queryClient.invalidateQueries({ queryKey: onboardingKeys.progress(userId) });
},
// Prevent duplicate requests by using the step name as a mutation key
mutationKey: (variables) => ['markStepCompleted', variables?.userId, variables?.stepName],
...options,
});
};
@@ -132,7 +132,7 @@ export const useResetProgress = (
options?: UseMutationOptions<UserProgress, ApiError, string>
) => {
const queryClient = useQueryClient();
return useMutation<UserProgress, ApiError, string>({
mutationFn: (userId: string) => onboardingService.resetProgress(userId),
onSuccess: (data, userId) => {
@@ -141,4 +141,110 @@ export const useResetProgress = (
},
...options,
});
};
// Draft Queries and Mutations
/**
* Query hook to get draft data for a specific step
*/
export const useStepDraft = (
stepName: string,
options?: Omit<UseQueryOptions<StepDraftResponse, ApiError>, 'queryKey' | 'queryFn'>
) => {
return useQuery<StepDraftResponse, ApiError>({
queryKey: onboardingKeys.stepDraft(stepName),
queryFn: () => onboardingService.getStepDraft(stepName),
enabled: !!stepName,
staleTime: 30 * 1000, // 30 seconds
...options,
});
};
/**
* Mutation hook to save draft data for a step
*/
export const useSaveStepDraft = (
options?: UseMutationOptions<{ success: boolean }, ApiError, { stepName: string; draftData: Record<string, any> }>
) => {
const queryClient = useQueryClient();
return useMutation<{ success: boolean }, ApiError, { stepName: string; draftData: Record<string, any> }>({
mutationFn: ({ stepName, draftData }) => onboardingService.saveStepDraft(stepName, draftData),
onSuccess: (_, { stepName }) => {
// Invalidate the draft query to get fresh data
queryClient.invalidateQueries({ queryKey: onboardingKeys.stepDraft(stepName) });
},
...options,
});
};
/**
* Mutation hook to delete draft data for a step
*/
export const useDeleteStepDraft = (
options?: UseMutationOptions<{ success: boolean }, ApiError, string>
) => {
const queryClient = useQueryClient();
return useMutation<{ success: boolean }, ApiError, string>({
mutationFn: (stepName: string) => onboardingService.deleteStepDraft(stepName),
onSuccess: (_, stepName) => {
// Invalidate the draft query
queryClient.invalidateQueries({ queryKey: onboardingKeys.stepDraft(stepName) });
},
...options,
});
};
/**
* Custom hook with debounced draft auto-save functionality.
* Automatically saves draft data after a delay when form data changes.
*/
export const useAutoSaveDraft = (stepName: string, debounceMs: number = 2000) => {
const saveStepDraft = useSaveStepDraft();
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const saveDraft = useCallback(
(draftData: Record<string, any>) => {
// Clear any existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Set a new timeout for debounced save
timeoutRef.current = setTimeout(() => {
saveStepDraft.mutate({ stepName, draftData });
}, debounceMs);
},
[stepName, debounceMs, saveStepDraft]
);
const saveDraftImmediately = useCallback(
(draftData: Record<string, any>) => {
// Clear any pending debounced save
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Save immediately
saveStepDraft.mutate({ stepName, draftData });
},
[stepName, saveStepDraft]
);
const cancelPendingSave = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}, []);
return {
saveDraft,
saveDraftImmediately,
cancelPendingSave,
isSaving: saveStepDraft.isPending,
isError: saveStepDraft.isError,
error: saveStepDraft.error,
};
};