webapps/console/components/SignInOrUp/FirebaseSignup.tsx
Rama newjitsu · réplica de lectura del repositorio original.
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<ReactNode | null>(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.{" "}
<Link className="font-bold underline" href={`/signin${queryStringCopy}`}>
Sign in
</Link>{" "}
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.{" "}
<Link className="font-bold underline" href={`/signin${queryStringCopy}`}>
Sign in
</Link>{" "}
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 (
<div className="flex min-h-screen">
<SignupMarketingPanel />
<div className="flex w-full flex-col bg-white lg:w-1/2 overflow-y-auto">
<div className="shrink-0 p-6 text-right text-sm text-textLight">
Already have an account?{" "}
<Link className="font-semibold text-primary" href={`/signin${queryStringCopy}`}>
Sign in →
</Link>
</div>
<div className="flex flex-1 justify-center px-6">
<div className="w-full max-w-[420px] py-6">
<div className="mb-4 h-10 w-10 drop-shadow-sm">{branding.logo}</div>
<h1 className="font-header text-2xl xl:text-3xl font-bold leading-tight text-textDark">
Join 8,000+ teams shipping data with Jitsu.
</h1>
<p className="mt-2 text-textLight">Free for 200k events/month. No credit card required.</p>
{error && (
<Alert className="mt-4" type="error" showIcon message={error} closable onClose={() => setError(null)} />
)}
<div className="mt-4">
<OAuthButtons
prefix="Continue"
providers={["firebase-google", "firebase-github"]}
onSSOLogin={handleSSOLogin}
notes={
appConfig.limitPersonalEmails
? {
"firebase-google": (
<Tooltip title="Work email required">
<span className="flex h-5 w-5 cursor-help items-center justify-center rounded-full bg-primary text-white shadow-sm ring-2 ring-white">
<Briefcase className="h-3 w-3" />
</span>
</Tooltip>
),
}
: undefined
}
/>
</div>
<Divider plain className="!my-4">
<span className="text-xs uppercase tracking-widest text-textLight">or with email</span>
</Divider>
<div className="space-y-3">
<div>
<label className="mb-1.5 block text-sm font-semibold text-textDark">Work email</label>
<Input
size="large"
variant="filled"
type="email"
autoComplete="email"
placeholder="you@company.com"
value={email}
onChange={e => {
setEmail(e.target.value);
setError(null);
setEmailConfirmed(false);
}}
onPressEnter={handleContinue}
disabled={submitting}
/>
</div>
{!emailConfirmed && (
<Button
block
type="primary"
size="large"
loading={checking}
onClick={handleContinue}
disabled={!email || !email.includes("@") || checking}
>
Continue →
</Button>
)}
{emailConfirmed && (
<>
<div>
<div className="mb-1.5 flex items-center justify-between">
<label className="text-sm font-semibold text-textDark">Password</label>
<span className="text-xs text-textLight">min. 6 chars</span>
</div>
<Input.Password
size="large"
variant="filled"
autoFocus
autoComplete="new-password"
placeholder="At least 6 characters"
value={password}
onChange={e => {
setPassword(e.target.value);
setError(null);
}}
disabled={submitting}
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-semibold text-textDark">Confirm password</label>
<Input.Password
size="large"
variant="filled"
autoComplete="new-password"
placeholder="Re-enter password"
value={confirm}
onChange={e => {
setConfirm(e.target.value);
setError(null);
}}
onPressEnter={handleSignup}
disabled={submitting}
/>
</div>
<Button
block
type="primary"
size="large"
loading={submitting}
onClick={handleSignup}
disabled={!password || !confirm}
>
Create account →
</Button>
</>
)}
<p className="text-center text-xs text-textLight">
By signing up, you agree to our{" "}
<a
className="underline hover:text-primary"
href="https://jitsu.com/tos"
target="_blank"
rel="noreferrer"
>
Terms of Service
</a>{" "}
and{" "}
<a
className="underline hover:text-primary"
href="https://jitsu.com/privacy"
target="_blank"
rel="noreferrer"
>
Privacy Policy
</a>
.
</p>
</div>
</div>
</div>
</div>
</div>
);
};