Authentication Flow
A complete authentication flow with login, registration, and password reset using individual stage components and StageRenderer.
Overview
This example demonstrates a comprehensive authentication system with the following features:
- Authentication Flow: Complete login/register/forgot password flow
- Individual Stage Components: Each stage is a separate React component
- StageRenderer: Automatic stage rendering based on current stage
- Credential Handling: Secure management of username/password
- Async Authentication: Simulated API calls with loading states
- Error States: Proper error handling for invalid credentials
- Success Flow: Welcome message and logout functionality
- State Management: Clean state management and transitions
Key Features to Observe
- Individual Stage Components: Each stage (login, register, forgot, loading, dashboard) is a separate component
- StageRenderer Usage: Automatic stage rendering without conditional rendering
- Authentication Flow: Follow the stages: 'login' → 'loading' → 'dashboard'/'error'
- Credential Handling: See how username/password are managed
- Async Authentication: Watch the simulated API call during the 'loading' stage
- Error States: Observe how invalid credentials trigger the 'error' stage
- Success Flow: See the welcome message and logout functionality
- State Management: Notice how credentials are cleared when logging out
Live Example
Live Editor
// import { StageFlowEngine } from '@stage-flow/core'; // import { StageFlowProvider, useStageFlow } from '@stage-flow/react'; function AuthenticationFlow() { // Common UI Components function FormContainer({ children, title = "Authentication" }) { return ( <div style={{ padding: "20px", border: "1px solid #ddd", borderRadius: "8px", maxWidth: "400px", backgroundColor: "white", }} > <h2 style={{ margin: "0 0 20px 0", color: "#2c3e50" }}>{title}</h2> {children} </div> ); } function FormInput({ type = "text", placeholder, value, onChange, style = {} }) { const inputStyle = { width: "100%", padding: "10px", marginBottom: "10px", border: "1px solid #ddd", borderRadius: "4px", fontSize: "14px", backgroundColor: "white", color: "#2c3e50", ...style, }; return <input type={type} placeholder={placeholder} value={value || ""} onChange={onChange} style={inputStyle} />; } function FormButton({ onClick, children, variant = "primary", disabled = false, style = {} }) { const buttonStyles = { primary: { backgroundColor: "#007bff", color: "white", }, secondary: { backgroundColor: "#6c757d", color: "white", }, success: { backgroundColor: "#28a745", color: "white", }, danger: { backgroundColor: "#dc3545", color: "white", }, }; const buttonStyle = { padding: "10px 20px", border: "none", borderRadius: "4px", cursor: disabled ? "not-allowed" : "pointer", fontSize: "14px", fontWeight: "500", opacity: disabled ? 0.6 : 1, ...buttonStyles[variant], ...style, }; return ( <button onClick={onClick} style={buttonStyle} disabled={disabled}> {children} </button> ); } function ErrorDisplay({ error }) { if (!error) return null; return ( <div style={{ marginTop: "10px", padding: "10px", backgroundColor: "#f8d7da", border: "1px solid #f5c6cb", borderRadius: "4px", color: "#721c24", }} > {error} </div> ); } function LoadingSpinner() { return ( <div style={{ textAlign: "center", padding: "40px 0" }}> <h3 style={{ margin: "0 0 15px 0", color: "#333" }}>Loading...</h3> <div style={{ width: "40px", height: "40px", border: "4px solid #f3f3f3", borderTop: "4px solid #007bff", borderRadius: "50%", animation: "spin 1s linear infinite", margin: "0 auto", }} /> <style>{` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `}</style> </div> ); } function DebugInfo() { const { currentStage, data } = useStageFlow(); return ( <div style={{ marginBottom: "20px", padding: "15px", backgroundColor: "#f8f9fa", borderRadius: "4px", border: "1px solid #e9ecef", maxWidth: "400px", width: "100%", }} > <div style={{ marginBottom: "10px" }}> <h4 style={{ margin: "0", color: "#495057" }}>Debug Info:</h4> </div> <div> <p style={{ margin: "5px 0", fontSize: "12px", color: "#6c757d" }}> <strong>Current Stage:</strong> {currentStage} </p> <p style={{ margin: "5px 0", fontSize: "12px", color: "#6c757d" }}> <strong>Stage Components:</strong> login, register, forgot, loading, dashboard </p> <p style={{ margin: "5px 0", fontSize: "12px", color: "#6c757d" }}> <strong>Current Data:</strong> {JSON.stringify(data)} </p> </div> </div> ); } // Login stage component function LoginStage({ data, send }) { const { engine } = useStageFlow(); const handleLogin = React.useCallback(async () => { if (!data?.username || !data?.password) { engine.setStageData({ ...(data || {}), error: "Please enter both username and password", }); return; } const currentData = { ...(data || {}) }; send("login", { ...currentData, isLoading: true, error: "", }); // Simulate API call setTimeout(() => { if (currentData.username === "admin" && currentData.password === "password123") { send("success", { ...currentData, isLoading: false, user: { username: "admin", role: "admin" }, }); } else { send("error", { ...currentData, isLoading: false, error: "Invalid credentials", }); } }, 1000); }, [data, send, engine]); const updateFormData = React.useCallback( (field, value) => { engine.setStageData({ ...(data || {}), [field]: value, error: "" }); }, [data, engine] ); return ( <FormContainer> <h3 style={{ margin: "0 0 15px 0", color: "#2c3e50" }}>Login</h3> <FormInput placeholder="Username" value={data?.username} onChange={e => updateFormData("username", e.target.value)} /> <FormInput type="password" placeholder="Password" value={data?.password} onChange={e => updateFormData("password", e.target.value)} /> <div style={{ display: "flex", gap: "10px" }}> <FormButton onClick={handleLogin} style={{ flex: 1 }}> Login </FormButton> </div> <ErrorDisplay error={data?.error} /> <div style={{ marginTop: "15px", padding: "12px", backgroundColor: "#f8f9fa", borderRadius: "4px", border: "1px solid #e9ecef" }}> <p style={{ margin: "0", fontSize: "12px", color: "#495057" }}>Test account: admin / password123</p> </div> <div style={{ display: "flex", gap: "10px" }}> <FormButton onClick={() => send("goToRegister")} variant="success"> Register </FormButton> <FormButton onClick={() => send("goToForgot")} variant="secondary"> Forgot Password? </FormButton> </div> </FormContainer> ); } // Register stage component function RegisterStage({ data, send }) { const { engine } = useStageFlow(); const handleRegister = React.useCallback(async () => { if (!data?.username || !data?.password || !data?.confirmPassword) { engine.setStageData({ ...(data || {}), error: "Please fill in all fields", }); return; } if (data?.password !== data?.confirmPassword) { engine.setStageData({ ...(data || {}), error: "Passwords do not match", }); return; } const currentData = { ...(data || {}) }; send("register", { ...currentData, isLoading: true, error: "", }); // Simulate API call setTimeout(() => { send("success", { ...currentData, isLoading: false, user: { username: currentData.username, role: "user" }, }); }, 1000); }, [data, send, engine]); const updateFormData = React.useCallback( (field, value) => { engine.setStageData({ ...(data || {}), [field]: value, error: "" }); }, [data, engine] ); return ( <FormContainer> <h3 style={{ margin: "0 0 15px 0", color: "#2c3e50" }}>Register</h3> <FormInput placeholder="Username" value={data?.username} onChange={e => updateFormData("username", e.target.value)} /> <FormInput type="password" placeholder="Password" value={data?.password} onChange={e => updateFormData("password", e.target.value)} /> <FormInput type="password" placeholder="Confirm Password" value={data?.confirmPassword} onChange={e => updateFormData("confirmPassword", e.target.value)} style={{ marginBottom: "15px" }} /> <div style={{ display: "flex", gap: "10px" }}> <FormButton onClick={handleRegister} variant="success" style={{ flex: 1 }}> Register </FormButton> </div> <ErrorDisplay error={data?.error} /> <FormButton onClick={() => send("goToLogin")} variant="secondary" style={{ width: "100%", marginTop: "10px" }}> Back to Login </FormButton> </FormContainer> ); } // Forgot password stage component function ForgotStage({ data, send }) { const { engine } = useStageFlow(); const handleReset = React.useCallback(async () => { if (!data?.email) { engine.setStageData({ ...(data || {}), error: "Please enter your email", }); return; } const currentData = { ...(data || {}) }; send("reset", { ...currentData, isLoading: true, error: "", }); // Simulate API call setTimeout(() => { send("resetSuccess", { ...currentData, isLoading: false, message: "Password reset link sent to your email", }); }, 1000); }, [data, send, engine]); const updateFormData = React.useCallback( (field, value) => { engine.setStageData({ ...(data || {}), [field]: value, error: "" }); }, [data, engine] ); return ( <FormContainer> <h3 style={{ margin: "0 0 15px 0", color: "#2c3e50" }}>Reset Password</h3> {data?.message ? ( <div style={{ textAlign: "center" }}> <h4 style={{ margin: "0 0 15px 0", color: "#28a745" }}>Success!</h4> <p style={{ color: "#666", marginBottom: "20px" }}>{data.message}</p> <FormButton onClick={() => send("goToLogin")}>Back to Login</FormButton> </div> ) : ( <> <FormInput type="email" placeholder="Email" value={data?.email} onChange={e => updateFormData("email", e.target.value)} style={{ marginBottom: "15px" }} /> <div style={{ display: "flex", gap: "10px" }}> <FormButton onClick={handleReset} style={{ flex: 1 }}> Send Reset Link </FormButton> <FormButton onClick={() => send("goToLogin")} variant="secondary"> Back to Login </FormButton> </div> </> )} </FormContainer> ); } // Loading stage component function LoadingStage({ data, send }) { return ( <FormContainer> <LoadingSpinner /> </FormContainer> ); } // Dashboard stage component function DashboardStage({ data, send }) { const handleLogout = React.useCallback(() => { send("logout", { username: "", password: "", confirmPassword: "", email: "", error: "", isLoading: false, user: null, }); }, [send]); return ( <FormContainer> <h3 style={{ margin: "0 0 15px 0", color: "#2c3e50" }}>Welcome, {data?.user?.username}!</h3> <p style={{ color: "#666", marginBottom: "20px" }}>You have successfully logged in.</p> <FormButton onClick={handleLogout} variant="danger"> Logout </FormButton> </FormContainer> ); } // Create engine directly without useRef const engine = new StageFlowEngine({ initial: "login", data: { username: "", password: "", confirmPassword: "", email: "", error: "", isLoading: false, user: null, }, stages: [ { name: "login", transitions: [ { target: "loading", event: "login" }, { target: "register", event: "goToRegister" }, { target: "forgot", event: "goToForgot" }, { target: "login", event: "updateForm" }, ], }, { name: "register", transitions: [ { target: "loading", event: "register" }, { target: "login", event: "goToLogin" }, { target: "register", event: "updateForm" }, ], }, { name: "forgot", transitions: [ { target: "loading", event: "reset" }, { target: "login", event: "goToLogin" }, { target: "forgot", event: "updateForm" }, ], }, { name: "loading", transitions: [ { target: "dashboard", event: "success" }, { target: "login", event: "error" }, { target: "register", event: "error" }, { target: "forgot", event: "error" }, { target: "forgot", event: "resetSuccess" }, ], }, { name: "dashboard", transitions: [{ target: "login", event: "logout" }], }, ], }); engine.start(); return ( <StageFlowProvider engine={engine}> <DebugInfo /> <StageRenderer stageComponents={{ login: LoginStage, register: RegisterStage, forgot: ForgotStage, loading: LoadingStage, dashboard: DashboardStage, }} /> </StageFlowProvider> ); }
Result
Loading...
Debug Interface
The debug interface provides real-time state monitoring:
- Current Stage: Shows the active stage name
- Stage Components: Lists all available stage components
- Current Data: Displays the current form data in JSON format
This helps developers monitor form state and debug state transitions during development.