forked from Goutam/lynkeduppro-crm
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:
@@ -145,18 +145,13 @@ export function startSocial(provider: "google" | "microsoft" | "apple"): boolean
|
||||
if (url) { window.location.href = url; return true; }
|
||||
return false;
|
||||
}
|
||||
export function SocialButtons({ onPick, verb = "Continue" }: { onPick: (p: "google" | "microsoft" | "apple") => void; verb?: string }) {
|
||||
export function SocialButtons({ onPick, verb = "Continue" }: { onPick: (p: "google") => void; verb?: string }) {
|
||||
// Only Google is offered (Microsoft/Apple intentionally hidden).
|
||||
return (
|
||||
<div className="col gap-3">
|
||||
<button className="btn btn-oauth" type="button" onClick={() => onPick("google")}>
|
||||
<GoogleMark /> {verb} with Google
|
||||
</button>
|
||||
<button className="btn btn-oauth" type="button" onClick={() => onPick("microsoft")}>
|
||||
<MicrosoftMark /> {verb} with Microsoft / Outlook
|
||||
</button>
|
||||
<button className="btn btn-oauth" type="button" onClick={() => onPick("apple")}>
|
||||
<AppleMark /> {verb} with Apple
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
import { Icon, Avatar } from "./icons";
|
||||
import {
|
||||
StepBack, FlashNote, Badge, OtpBoxes, ResendLink, RememberDevice,
|
||||
SocialButtons, startSocial, PasswordStrength, LegalModal,
|
||||
PasswordStrength, LegalModal,
|
||||
} from "./bits";
|
||||
import {
|
||||
countryCodes, relationshipOptions, addressCountries,
|
||||
@@ -158,23 +158,13 @@ function StepAccount(p: {
|
||||
const [phase, setPhase] = useState<"sso" | "profile">("sso");
|
||||
const [modal, setModal] = useState<null | "terms" | "privacy">(null);
|
||||
|
||||
function pickSso(provider: "google" | "microsoft" | "apple") {
|
||||
if (startSocial(provider)) return;
|
||||
const mockEmail = `you@${provider === "microsoft" ? "outlook.com" : provider + ".com"}`;
|
||||
p.setSso({ provider, email: mockEmail });
|
||||
p.setEmail(mockEmail);
|
||||
setPhase("profile");
|
||||
}
|
||||
|
||||
if (phase === "sso") {
|
||||
const valid = EMAIL_RE.test(p.email);
|
||||
return (
|
||||
<div>
|
||||
<h1>Create your account</h1>
|
||||
<p className="sub">Sign up with a provider or your email to get started.</p>
|
||||
<p className="sub">Enter your email to get started.</p>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<SocialButtons onPick={pickSso} verb="Sign up" />
|
||||
<div className="divider">or continue with email</div>
|
||||
<form onSubmit={(e) => { e.preventDefault(); if (valid) { p.setSso(null); setPhase("profile"); } }}>
|
||||
<div className="field">
|
||||
<label className="label">Email address</label>
|
||||
|
||||
Reference in New Issue
Block a user