Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 870525d6c7 | |||
| 127e0f8912 |
@@ -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">
|
||||||
<div className="seg" style={{ margin: "16px 0 14px" }}>
|
{!shell
|
||||||
<button className={channel === "email" ? "on" : ""} onClick={() => { setChannel("email"); setError(""); setSent(false); }}><Icon name="mail" size={15} /> Email</button>
|
? "Enter the 6-digit code we sent you to finish signing in."
|
||||||
<button className={channel === "sms" ? "on" : ""} onClick={() => { setChannel("sms"); setError(""); setSent(false); }}><Icon name="sms" size={15} /> SMS</button>
|
: channel === "sms"
|
||||||
{!shell && <button className={channel === "wa" ? "on" : ""} onClick={() => { setChannel("wa"); setError(""); }}><Icon name="whatsapp" size={15} /> WhatsApp</button>}
|
? "We'll text a 6-digit code to sign in."
|
||||||
</div>
|
: "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" }}>
|
||||||
|
<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 === "wa" ? "on" : ""} onClick={() => { setChannel("wa"); setError(""); }}><Icon name="whatsapp" size={15} /> WhatsApp</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{shell && channel === "sms" && !sent ? (
|
{shell && channel === "sms" && !sent ? (
|
||||||
<form onSubmit={(e) => { e.preventDefault(); void send(); }}>
|
<form onSubmit={(e) => { e.preventDefault(); void send(); }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
Reference in New Issue
Block a user