import React, { ReactNode, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; import { Alert, Button, Divider, Input, Tooltip } from "antd"; import { branding } from "../../lib/branding"; import { useFirebaseSession } from "../../lib/firebase-client"; import { safeRedirect } from "../../lib/auth-redirect"; import { useJitsu } from "@jitsu/jitsu-react"; import { OAuthButtons } from "./OAuthButtons"; import { SignupMarketingPanel } from "./SignupMarketingPanel"; import { AuthType, handleFirebaseError } from "./SignInOrUp"; import { useQueryStringCopy } from "./use-query-string-copy"; import { getErrorMessage, rpc } from "juava"; import { CreateUserResult } from "../../lib/schema"; import { useAppConfig } from "../../lib/context"; import { Briefcase } from "lucide-react"; export const FirebaseSignup: React.FC<{ initialError?: ReactNode }> = ({ initialError }) => { const router = useRouter(); const firebaseSession = useFirebaseSession(); const { analytics } = useJitsu(); const queryStringCopy = useQueryStringCopy(); const appConfig = useAppConfig(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirm, setConfirm] = useState(""); const [error, setError] = useState(initialError ?? null); const [submitting, setSubmitting] = useState(false); const [checking, setChecking] = useState(false); const [emailConfirmed, setEmailConfirmed] = useState(false); const callbackUrl = (router.query.callbackUrl as string) || "/"; // Step 1 → 2: reveal the password fields only once a plausible email is // entered. JITSU-70: ask the server whether this email may sign up before // creating any Firebase account, so a personal-email signup is refused here // rather than after a wasted verification email. const handleContinue = async () => { if (!email || !email.includes("@")) { setError("Please enter a valid email address"); return; } setError(null); setChecking(true); try { const result: CreateUserResult = await rpc("/api/auth/signup-email-check", { body: { email } }); if (!result.ok && result.rejected === "personal-email") { setError(result.message); return; } setEmailConfirmed(true); } catch (e: any) { setError(getErrorMessage(e) || "Something went wrong, please try again"); } finally { setChecking(false); } }; const validate = (): string | null => { if (!email || !email.includes("@")) { return "Please enter a valid email address"; } if (password.length < 6) { return "Password must be at least 6 characters"; } if (password !== confirm) { return "Passwords do not match"; } return null; }; const mapSignupError = (e: any): ReactNode => { const code = e?.code; if (code === "auth/email-already-in-use") { return ( <> An account with this email already exists.{" "} Sign in {" "} instead. ); } if (code === "auth/weak-password") { return "Password is too weak. Use at least 6 characters."; } if (code === "auth/invalid-email") { return "Please enter a valid email address"; } return handleFirebaseError(e); }; const handleSignup = async () => { const validationError = validate(); if (validationError) { setError(validationError); return; } setSubmitting(true); setError(null); try { await firebaseSession.signUp(email, password); await analytics.track("sign_up", { email, loginProvider: "firebase/email" }); // The new account is single-provider `password` + unverified, so the route // gate (FirebaseAuthorizer) takes over and renders VerifyEmailGate. safeRedirect(router, callbackUrl); } catch (e: any) { setError(mapSignupError(e)); await analytics.track("sign_up_error", { email, loginProvider: "firebase/email", message: e?.message || "Unknown error", }); } finally { setSubmitting(false); } }; // OAuth signup is the same popup path as sign-in — Firebase creates the account // on first login. signInWith already records the login and fires analytics. const handleSSOLogin = async (provider: AuthType) => { setError(null); try { const result = await firebaseSession.signInWith(provider === "firebase-google" ? "google.com" : "github.com"); // JITSU-70: the server refused a personal-email Google signup and already // deleted the Firebase account. Show the message and clear the now-stale // client session so a retry with a work email starts clean. if (result.status === "personal-email-rejected") { setError(result.message); await firebaseSession.signOut(); return; } if (result.status === "account-exists-different-credential") { // JITSU-244: this is signup, not sign-in — point the user at sign-in // (where the account-linking prompt lives) rather than building a // second copy of that flow here. The credential is simply dropped; // signing in with GitHub/Google again from /signin re-derives it. setError( <> An account with this email already exists.{" "} Sign in {" "} instead. ); return; } safeRedirect(router, callbackUrl); } catch (e: any) { setError(handleFirebaseError(e)); await analytics.track("login_error", { type: "social", loginProvider: `firebase/${provider}`, message: e?.message || "Unknown error", }); } }; return (
Already have an account?{" "} Sign in →
{branding.logo}

Join 8,000+ teams shipping data with Jitsu.

Free for 200k events/month. No credit card required.

{error && ( setError(null)} /> )}
), } : undefined } />
or with email
{ setEmail(e.target.value); setError(null); setEmailConfirmed(false); }} onPressEnter={handleContinue} disabled={submitting} />
{!emailConfirmed && ( )} {emailConfirmed && ( <>
min. 6 chars
{ setPassword(e.target.value); setError(null); }} disabled={submitting} />
{ setConfirm(e.target.value); setError(null); }} onPressEnter={handleSignup} disabled={submitting} />
)}

By signing up, you agree to our{" "} Terms of Service {" "} and{" "} Privacy Policy .

); };