feat(auth): Google-only social; sign-up email/password only; phone login entry

- SocialButtons: only Google (Microsoft/Apple hidden).
- Sign-up: remove all social (email/password only).
- Login: keep Google + email/password (single step) + email/SMS OTP; add a direct
  'Sign in with a phone number' entry (SMS OTP), and rename the OTP link to
  'Sign in with a one-time code'. All Supabase-supported; no mandatory 2FA.
This commit is contained in:
tanweer919
2026-07-11 20:48:47 +05:30
parent 339e2b007a
commit 74caae0710
3 changed files with 29 additions and 28 deletions
+25 -9
View File
@@ -31,6 +31,18 @@ export function LoginFlow() {
const [provider, setProvider] = useState<string>("");
const [remember, setRemember] = useState(false);
const [flash, setFlash] = useState<string>("");
const [otpChannel, setOtpChannel] = useState<"email" | "sms">("email");
// Minimal account stand-in for passwordless entry points (Shell mode).
function blankAccount(): Account {
return { firstName: "", name: "", initials: "", hasPasskey: false, hasTotp: false, hasPush: false, maskedEmail: maskEmail(email || ""), maskedPhone: "", maskedWa: "" };
}
// Direct "sign in with phone" → the one-time-code screen, SMS preselected.
function startPhoneLogin() {
setAccount(blankAccount());
setOtpChannel("sms");
replace("otp");
}
/* ---- history hash sync ---- */
function push(s: Step) {
@@ -100,6 +112,7 @@ export function LoginFlow() {
hasPasskey: false, hasTotp: false, hasPush: false,
maskedEmail: maskEmail(email), maskedPhone: "", maskedWa: "",
});
setOtpChannel("email");
replace("password");
return;
}
@@ -116,7 +129,7 @@ export function LoginFlow() {
/* =================================================================== */
return (
<div className="card anim-fade-up" key={step}>
{step === "identify" && <Identify email={email} setEmail={setEmail} onSocial={onSocial} onEmail={identifyEmail} toRegister={() => router.push("/portal/register")} />}
{step === "identify" && <Identify email={email} setEmail={setEmail} onSocial={onSocial} onEmail={identifyEmail} onPhone={startPhoneLogin} toRegister={() => router.push("/portal/register")} />}
{step === "connecting" && (
<div className="interstitial">
@@ -162,11 +175,11 @@ export function LoginFlow() {
)}
{step === "password" && account && (
<Password account={account} email={email} login={login} onAuthenticated={() => router.replace("/dashboard")} flash={flash} onBack={back} onForgot={() => push("fp_confirm")} onOtp={() => replace("otp")} onLocked={() => setFlash("This account is temporarily locked.")} onOk={() => afterAuth("password")} />
<Password account={account} email={email} login={login} onAuthenticated={() => router.replace("/dashboard")} flash={flash} onBack={back} onForgot={() => push("fp_confirm")} onOtp={() => { setOtpChannel("email"); replace("otp"); }} onLocked={() => setFlash("This account is temporarily locked.")} onOk={() => afterAuth("password")} />
)}
{step === "otp" && account && (
<OtpVerify account={account} email={email} remember={remember} setRemember={setRemember} onBack={back} onVerified={() => afterAuth("otp")} onAuthenticated={() => router.replace("/dashboard")} />
<OtpVerify account={account} email={email} initialChannel={otpChannel} remember={remember} setRemember={setRemember} onBack={back} onVerified={() => afterAuth("otp")} onAuthenticated={() => router.replace("/dashboard")} />
)}
{step === "another" && account && (
@@ -230,9 +243,9 @@ export function LoginFlow() {
/* ============================ screens ============================ */
function Identify({ email, setEmail, onSocial, onEmail, toRegister }: {
function Identify({ email, setEmail, onSocial, onEmail, onPhone, toRegister }: {
email: string; setEmail: (v: string) => void;
onSocial: (p: "google" | "microsoft" | "apple") => void; onEmail: () => void; toRegister: () => void;
onSocial: (p: "google") => void; onEmail: () => void; onPhone: () => void; toRegister: () => void;
}) {
const [showEmail, setShowEmail] = useState(false);
const valid = EMAIL_RE.test(email);
@@ -263,6 +276,9 @@ function Identify({ email, setEmail, onSocial, onEmail, toRegister }: {
</button>
</form>
)}
{isShellConfigured() && (
<button className="link" style={{ marginTop: 14, display: "block" }} onClick={onPhone}><Icon name="sms" size={15} /> Sign in with a phone number instead</button>
)}
</div>
<p className="foot-note">New homeowner or contractor? <button className="link" onClick={toRegister}>Register here</button></p>
@@ -357,20 +373,20 @@ function Password({ account, email, login, onAuthenticated, flash, onBack, onFor
</form>
<div className="row between" style={{ marginTop: 16 }}>
<button className="link" onClick={onForgot}>Forgot password?</button>
<button className="link" onClick={onOtp}>Sign in using email OTP</button>
<button className="link" onClick={onOtp}>Sign in with a one-time code</button>
</div>
{!isShellConfigured() && <p className="demo-hint">Demo: any password works; type wrong to see the lockout. Password always needs 2-step next.</p>}
</div>
);
}
function OtpVerify({ account, email, remember, setRemember, onBack, onVerified, onAuthenticated }: {
account: Account; email: string; remember: boolean; setRemember: (v: boolean) => void; onBack: () => void; onVerified: () => void; onAuthenticated: () => void;
function OtpVerify({ account, email, initialChannel = "email", remember, setRemember, onBack, onVerified, onAuthenticated }: {
account: Account; email: string; initialChannel?: "email" | "sms"; remember: boolean; setRemember: (v: boolean) => void; onBack: () => void; onVerified: () => void; onAuthenticated: () => void;
}) {
const { sendEmailOtp, verifyEmailOtp, sendPhoneOtp, verifyPhoneOtp } = useAuth();
const shell = isShellConfigured();
// In Shell mode only email + SMS are real Supabase OTP channels; hide WhatsApp.
const [channel, setChannel] = useState<"email" | "sms" | "wa">("email");
const [channel, setChannel] = useState<"email" | "sms" | "wa">(initialChannel);
const [phone, setPhone] = useState("");
const [sent, setSent] = useState(false);
const [busy, setBusy] = useState(false);