fix(login): back nav, single OTP channel, register back buttons #18

Merged
tanweer919 merged 1 commits from tanweer919/lynkeduppro-crm:feat/login-methods into goutamnextflow 2026-07-13 09:02:36 +00:00
3 changed files with 44 additions and 15 deletions
Showing only changes of commit 127e0f8912 - Show all commits
+19 -5
View File
@@ -11,6 +11,7 @@ import {
import { lookupAccount, maskEmail, type Account } from "./data"; import { lookupAccount, maskEmail, type Account } from "./data";
import { useAuth, useAppShell } from "@abe-kap/appshell-sdk/react"; import { useAuth, useAppShell } from "@abe-kap/appshell-sdk/react";
import { isShellConfigured } from "@/lib/appshell"; import { isShellConfigured } from "@/lib/appshell";
import { MOCK_OTP } from "@/lib/otp";
// Map the portal's social button ids to Supabase OAuth provider ids. // Map the portal's social button ids to Supabase OAuth provider ids.
const OAUTH_PROVIDER: Record<string, string> = { google: "google", microsoft: "azure", apple: "apple" }; const OAUTH_PROVIDER: Record<string, string> = { google: "google", microsoft: "azure", apple: "apple" };
@@ -42,7 +43,7 @@ export function LoginFlow() {
function startPhoneLogin() { function startPhoneLogin() {
setAccount(blankAccount()); setAccount(blankAccount());
setOtpChannel("sms"); setOtpChannel("sms");
replace("otp"); push("otp"); // push (not replace) so Back returns to the identify screen
} }
/* ---- history hash sync ---- */ /* ---- history hash sync ---- */
@@ -134,7 +135,7 @@ export function LoginFlow() {
maskedEmail: maskEmail(email), maskedPhone: "", maskedWa: "", maskedEmail: maskEmail(email), maskedPhone: "", maskedWa: "",
}); });
setOtpChannel("email"); setOtpChannel("email");
replace("password"); push("password"); // push (not replace) so Back returns to the email screen, not off-page
return; return;
} }
push("connecting"); push("connecting");
@@ -301,7 +302,10 @@ function Identify({ email, setEmail, onSocial, onEmail, onPhone, toRegister }: {
</button> </button>
</form> </form>
)} )}
{isShellConfigured() && ( {/* Phone (SMS) sign-in needs a deliverable one-time code — hidden while SMS is
mocked, since a faked code can't mint a real session. Email + password + Google
all work without SMS. */}
{isShellConfigured() && !MOCK_OTP && (
<button className="link" style={{ marginTop: 14, display: "block" }} onClick={onPhone}><Icon name="sms" size={15} /> Sign in with a phone number instead</button> <button className="link" style={{ marginTop: 14, display: "block" }} onClick={onPhone}><Icon name="sms" size={15} /> Sign in with a phone number instead</button>
)} )}
</div> </div>
@@ -450,12 +454,22 @@ function OtpVerify({ account, email, initialChannel = "email", remember, setReme
<div> <div>
<StepBack onClick={onBack} /> <StepBack onClick={onBack} />
<h1>{shell ? "One-time passcode" : "2-step verification"}</h1> <h1>{shell ? "One-time passcode" : "2-step verification"}</h1>
<p className="sub">{shell ? "We'll text or email you a 6-digit code to sign in." : "Enter the 6-digit code we sent you to finish signing in."}</p> <p className="sub">
{!shell
? "Enter the 6-digit code we sent you to finish signing in."
: channel === "sms"
? "We'll text a 6-digit code to sign in."
: "We'll email you a 6-digit code to sign in."}
</p>
{/* In Shell mode the channel is fixed by how you signed in (email vs phone) —
no toggle, so an email sign-in never shows a stray SMS option. */}
{!shell && (
<div className="seg" style={{ margin: "16px 0 14px" }}> <div className="seg" style={{ margin: "16px 0 14px" }}>
<button className={channel === "email" ? "on" : ""} onClick={() => { setChannel("email"); setError(""); setSent(false); }}><Icon name="mail" size={15} /> Email</button> <button className={channel === "email" ? "on" : ""} onClick={() => { setChannel("email"); setError(""); setSent(false); }}><Icon name="mail" size={15} /> Email</button>
<button className={channel === "sms" ? "on" : ""} onClick={() => { setChannel("sms"); setError(""); setSent(false); }}><Icon name="sms" size={15} /> SMS</button> <button className={channel === "sms" ? "on" : ""} onClick={() => { setChannel("sms"); setError(""); setSent(false); }}><Icon name="sms" size={15} /> SMS</button>
{!shell && <button className={channel === "wa" ? "on" : ""} onClick={() => { setChannel("wa"); setError(""); }}><Icon name="whatsapp" size={15} /> WhatsApp</button>} <button className={channel === "wa" ? "on" : ""} onClick={() => { setChannel("wa"); setError(""); }}><Icon name="whatsapp" size={15} /> WhatsApp</button>
</div> </div>
)}
{shell && channel === "sms" && !sent ? ( {shell && channel === "sms" && !sent ? (
<form onSubmit={(e) => { e.preventDefault(); void send(); }}> <form onSubmit={(e) => { e.preventDefault(); void send(); }}>
+5 -6
View File
@@ -13,18 +13,13 @@ import {
} from "./data"; } from "./data";
import { useAuth, useAppShell } from "@abe-kap/appshell-sdk/react"; import { useAuth, useAppShell } from "@abe-kap/appshell-sdk/react";
import { isShellConfigured } from "@/lib/appshell"; import { isShellConfigured } from "@/lib/appshell";
import { MOCK_OTP, DEMO_OTP } from "@/lib/otp";
type Addr = { line1?: string; line2?: string; city?: string; state?: string; postalCode?: string; country?: string; locality?: string }; type Addr = { line1?: string; line2?: string; city?: string; state?: string; postalCode?: string; country?: string; locality?: string };
const STEPS = ["Account", "Verify", "Address"]; const STEPS = ["Account", "Verify", "Address"];
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// Demo fallback for phone OTP while the Twilio sender is down. When on, the Verify
// step skips Supabase/Twilio and accepts a fixed code. Flip NEXT_PUBLIC_MOCK_OTP to
// "false" (or remove it) to restore real SMS verification — no other change needed.
const MOCK_OTP = process.env.NEXT_PUBLIC_MOCK_OTP === "true";
const DEMO_OTP = "123456";
export function RegisterFlow({ mode = "register" }: { mode?: "register" | "onboard" }) { export function RegisterFlow({ mode = "register" }: { mode?: "register" | "onboard" }) {
// "onboard" = an already-authenticated OAuth (Google) user completing their CRM // "onboard" = an already-authenticated OAuth (Google) user completing their CRM
// profile: email is skipped (Google-verified), the auth account already exists so // profile: email is skipped (Google-verified), the auth account already exists so
@@ -214,6 +209,7 @@ function StepAccount(p: {
const valid = EMAIL_RE.test(p.email); const valid = EMAIL_RE.test(p.email);
return ( return (
<div> <div>
<StepBack onClick={p.toLogin} />
<h1>Create your account</h1> <h1>Create your account</h1>
<p className="sub">Enter your email to get started.</p> <p className="sub">Enter your email to get started.</p>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
@@ -235,6 +231,9 @@ function StepAccount(p: {
return ( return (
<div> <div>
{/* Non-onboarding users can step back to the email screen; onboarding starts
here (email came from Google), so there's nothing to go back to. */}
{!p.onboard && <StepBack onClick={() => setPhase("sso")} />}
<h1>Complete your profile</h1> <h1>Complete your profile</h1>
<p className="sub">Tell us a bit about you to set up your account.</p> <p className="sub">Tell us a bit about you to set up your account.</p>
+16
View File
@@ -0,0 +1,16 @@
/**
* Demo OTP fallback, shared by the login and registration flows.
*
* While the Twilio SMS sender is down we can't deliver real one-time codes. When
* MOCK_OTP is on, phone verification steps skip Supabase/Twilio and accept a fixed
* DEMO_OTP instead. Flip NEXT_PUBLIC_MOCK_OTP to "false" (or remove it) to restore
* real SMS — no other change needed.
*
* IMPORTANT: this only substitutes for a *secondary* verification (e.g. confirming a
* phone during registration/onboarding, where the session already exists). It cannot
* mock a *login* whose sole credential is the OTP — there the OTP verification is what
* mints the session, and a faked code produces no session (the dashboard's AuthGate
* would bounce the user straight back). Passwordless login therefore stays real.
*/
export const MOCK_OTP = process.env.NEXT_PUBLIC_MOCK_OTP === "true";
export const DEMO_OTP = "123456";