|
|
|
@@ -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);
|
|
|
|
|