web-production-saas-starter/next_b2b_starter/hooks/use-signup-flow.ts
2025-12-16 18:54:41 +04:00

171 lines
4.4 KiB
TypeScript

import { useCallback, useMemo, useState } from "react";
import { signupRepository } from "@/lib/api/api/repositories/signup-repository";
import {
SignupOrganization,
SignupOwner,
SignupResult,
} from "@/lib/models/signup.model";
export type SignupStep = "account" | "organization";
interface UseSignupFlowState {
step: SignupStep;
owner: SignupOwner;
organization: SignupOrganization;
isLoading: boolean;
error: string | null;
emailSent: boolean;
result: SignupResult | null;
stepIndex: number;
canContinueAccount: boolean;
canContinueOrganization: boolean;
goBack: () => void;
goNext: () => void;
sendMagicLink: () => Promise<void>;
updateOwner: (updates: Partial<SignupOwner>) => void;
updateOrganization: (updates: Partial<SignupOrganization>) => void;
reset: () => void;
}
const defaultOwner: SignupOwner = {
fullName: "",
email: "",
};
const defaultOrganization: SignupOrganization = {
displayName: "",
industry: "Technology",
};
export function useSignupFlow(): UseSignupFlowState {
const [step, setStep] = useState<SignupStep>("account");
const [owner, setOwner] = useState<SignupOwner>(defaultOwner);
const [organization, setOrganization] = useState<SignupOrganization>(
defaultOrganization
);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [emailSent, setEmailSent] = useState(false);
const [result, setResult] = useState<SignupResult | null>(null);
const stepIndex = useMemo(() => {
switch (step) {
case "account":
return 0;
case "organization":
return 1;
default:
return 0;
}
}, [step]);
const canContinueAccount = useMemo(() => {
return (
owner.fullName.trim().length >= 2 &&
/.+@.+\..+/.test(owner.email)
);
}, [owner]);
const canContinueOrganization = useMemo(() => {
return (
organization.displayName.trim().length >= 2 &&
organization.industry.trim().length > 0
);
}, [organization]);
const goBack = useCallback(() => {
setError(null);
if (step === "organization") {
setStep("account");
}
}, [step]);
const goNext = useCallback(() => {
setError(null);
if (step === "account" && canContinueAccount) {
setStep("organization");
}
}, [step, canContinueAccount]);
const updateOwner = useCallback((updates: Partial<SignupOwner>) => {
setOwner((prev) => ({ ...prev, ...updates }));
setError(null); // Clear error when user types
}, []);
const updateOrganization = useCallback((updates: Partial<SignupOrganization>) => {
setOrganization((prev) => ({ ...prev, ...updates }));
setError(null); // Clear error when user types
}, []);
const reset = useCallback(() => {
setOwner(defaultOwner);
setOrganization(defaultOrganization);
setStep("account");
setError(null);
setEmailSent(false);
setResult(null);
}, []);
const sendMagicLink = useCallback(async () => {
if (!canContinueOrganization) {
setError("Please fill in all required fields correctly");
return;
}
setIsLoading(true);
setError(null);
try {
// Call the API to create organization and send magic link
const signupResult = await signupRepository.createOrganizationWithMagicLink(
owner,
organization
);
setResult(signupResult);
setEmailSent(true);
// Immediately send magic link via login endpoint (bypass backend async)
try {
await fetch("/api/auth/magic-link", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email: owner.email }),
});
} catch (magicLinkError) {
console.error("Failed to send magic link:", magicLinkError);
// Don't block signup - user can retry login if needed
}
} catch (signupError) {
const message =
signupError instanceof Error
? signupError.message
: "Failed to send magic link. Please try again.";
setError(message);
setEmailSent(false);
} finally {
setIsLoading(false);
}
}, [owner, organization, canContinueOrganization]);
return {
step,
owner,
organization,
isLoading,
error,
emailSent,
result,
stepIndex,
canContinueAccount,
canContinueOrganization,
goBack,
goNext,
sendMagicLink,
updateOwner,
updateOrganization,
reset,
};
}