forked from Goutam/lynkeduppro-crm
feat(auth): first-time Google onboarding (option 3)
After a Google sign-in with no CRM profile, route to /portal/onboarding — the registration flow in mode='onboard': email skipped (Google-verified, prefilled via getUserEmail), sets a password (updateUser) so email+password login also works, skips the OTP-verify step, and persists the profile via crm.account.register. Existing-profile users go straight to the dashboard. Bumps SDK ^0.2.3.
This commit is contained in:
@@ -19,9 +19,13 @@ type Addr = { line1?: string; line2?: string; city?: string; state?: string; pos
|
||||
const STEPS = ["Account", "Verify", "Address"];
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
export function RegisterFlow() {
|
||||
export function RegisterFlow({ mode = "register" }: { mode?: "register" | "onboard" }) {
|
||||
// "onboard" = an already-authenticated OAuth (Google) user completing their CRM
|
||||
// profile: email is skipped (Google-verified), the auth account already exists so
|
||||
// we only SET a password + persist the profile, and the OTP-verify step is skipped.
|
||||
const onboard = mode === "onboard";
|
||||
const router = useRouter();
|
||||
const { register } = useAuth();
|
||||
const { register, setPassword, getUserEmail } = useAuth();
|
||||
const { sdk } = useAppShell();
|
||||
const [step, setStep] = useState(0);
|
||||
const [submitErr, setSubmitErr] = useState("");
|
||||
@@ -50,6 +54,15 @@ export function RegisterFlow() {
|
||||
const [emailVerified, setEmailVerified] = useState(false);
|
||||
const [phoneVerified, setPhoneVerified] = useState(false);
|
||||
|
||||
// Onboarding: prefill the verified email from the Google session (the ACE omits it).
|
||||
useEffect(() => {
|
||||
if (!onboard) return;
|
||||
getUserEmail().then((e) => { if (e) setEmail(e); }).catch(() => { /* ignore */ });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onboard]);
|
||||
|
||||
const STEP_LABELS = onboard ? ["Profile", "Address"] : STEPS;
|
||||
|
||||
const isSelf = relationship === "Customer" || relationship === "Owner";
|
||||
const isEmployee = relationship === "Employee";
|
||||
const country = countryCodes.find((c) => c.code === cc)!;
|
||||
@@ -76,8 +89,11 @@ export function RegisterFlow() {
|
||||
|
||||
if (isShellConfigured()) {
|
||||
setSubmitErr("");
|
||||
// 1) Auth account — appshell → Supabase (SSO users already have a session).
|
||||
if (!sso) {
|
||||
// 1) Auth account. Onboarding users are already authenticated via Google — set
|
||||
// a password so email+password login works too. Everyone else registers now.
|
||||
if (onboard) {
|
||||
try { if (pw) await setPassword(pw); } catch { /* non-fatal — the profile still saves */ }
|
||||
} else if (!sso) {
|
||||
try { await register(finalEmail, pw); }
|
||||
catch { setSubmitErr("We couldn't create that account. The email may already be registered."); return; }
|
||||
}
|
||||
@@ -98,7 +114,8 @@ export function RegisterFlow() {
|
||||
consentTerms: termsOk, consentPrivacy: privacyOk,
|
||||
});
|
||||
} catch {
|
||||
// Non-fatal: the auth account already exists; the domain profile can be filled in later.
|
||||
if (onboard) { setSubmitErr("Couldn't save your profile. Please try again."); return; }
|
||||
// register mode: non-fatal — the auth account exists; the profile can be filled in later.
|
||||
}
|
||||
}
|
||||
router.push("/dashboard");
|
||||
@@ -106,10 +123,11 @@ export function RegisterFlow() {
|
||||
|
||||
return (
|
||||
<div className="card anim-fade-up">
|
||||
<Stepper current={step} />
|
||||
<Stepper current={step} labels={STEP_LABELS} />
|
||||
|
||||
{step === 0 && (
|
||||
<StepAccount
|
||||
onboard={onboard}
|
||||
sso={sso} setSso={setSso} email={email} setEmail={setEmail}
|
||||
first={first} setFirst={setFirst} last={last} setLast={setLast}
|
||||
cc={cc} setCc={setCc} phone={phone} setPhone={setPhone} phoneOk={phoneOk} country={country}
|
||||
@@ -123,7 +141,7 @@ export function RegisterFlow() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 1 && (
|
||||
{step === 1 && !onboard && (
|
||||
<StepVerify
|
||||
emailValue={sso?.email || email} cc={cc} phone={phone} country={country}
|
||||
emailVerified={emailVerified} setEmailVerified={setEmailVerified}
|
||||
@@ -132,10 +150,10 @@ export function RegisterFlow() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
{((onboard && step === 1) || (!onboard && step === 2)) && (
|
||||
<>
|
||||
{submitErr && <div style={{ marginBottom: 14 }}><FlashNote tone="error">{submitErr}</FlashNote></div>}
|
||||
<StepAddress sameAs={mailingSame} setSameAs={setMailingSame} onRegAddr={setRegAddr} onMailAddr={setMailAddr} onBack={() => setStep(1)} onFinish={finish} />
|
||||
<StepAddress sameAs={mailingSame} setSameAs={setMailingSame} onRegAddr={setRegAddr} onMailAddr={setMailAddr} onBack={() => setStep(onboard ? 0 : 1)} onFinish={finish} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -153,9 +171,10 @@ function StepAccount(p: {
|
||||
alloeNo: string; setAlloeNo: (v: string) => void; alloeIdOk: boolean;
|
||||
alloeFirst: string; setAlloeFirst: (v: string) => void; alloeLast: string; setAlloeLast: (v: string) => void;
|
||||
termsOk: boolean; setTermsOk: (v: boolean) => void; privacyOk: boolean; setPrivacyOk: (v: boolean) => void;
|
||||
valid: boolean; onContinue: () => void; toLogin: () => void;
|
||||
valid: boolean; onContinue: () => void; toLogin: () => void; onboard?: boolean;
|
||||
}) {
|
||||
const [phase, setPhase] = useState<"sso" | "profile">("sso");
|
||||
// Onboarding (OAuth) starts on the profile step — email is already known + verified.
|
||||
const [phase, setPhase] = useState<"sso" | "profile">(p.onboard ? "profile" : "sso");
|
||||
const [modal, setModal] = useState<null | "terms" | "privacy">(null);
|
||||
|
||||
if (phase === "sso") {
|
||||
@@ -190,7 +209,7 @@ function StepAccount(p: {
|
||||
{p.sso ? (
|
||||
<div className="conn-banner conn-green"><Icon name="check" size={16} /> Connected with {cap(p.sso.provider)} · {p.sso.email}</div>
|
||||
) : (
|
||||
<div className="conn-banner conn-blue"><Icon name="mail" size={16} /> Creating account for {p.email}</div>
|
||||
<div className="conn-banner conn-blue"><Icon name="mail" size={16} /> {p.onboard ? "Signed in as" : "Creating account for"} {p.email}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -289,7 +308,7 @@ function StepAccount(p: {
|
||||
|
||||
{!p.valid && <p className="hint-line">Fill all required fields and accept both documents to continue.</p>}
|
||||
|
||||
<button className="btn btn-primary" style={{ marginTop: 14 }} disabled={!p.valid} onClick={p.onContinue}>Create Account & Verify <Icon name="arrowR" size={16} /></button>
|
||||
<button className="btn btn-primary" style={{ marginTop: 14 }} disabled={!p.valid} onClick={p.onContinue}>{p.onboard ? "Continue" : "Create Account & Verify"} <Icon name="arrowR" size={16} /></button>
|
||||
|
||||
{modal === "terms" && <LegalModal doc={TERMS} onClose={() => setModal(null)} onReviewed={() => { reviewed.terms = true; setModal(null); }} />}
|
||||
{modal === "privacy" && <LegalModal doc={PRIVACY} onClose={() => setModal(null)} onReviewed={() => { reviewed.privacy = true; setModal(null); }} />}
|
||||
@@ -581,10 +600,10 @@ function AddressBlock({ idPrefix, onChange }: { idPrefix: string; onChange?: (a:
|
||||
}
|
||||
|
||||
/* ====================== stepper ====================== */
|
||||
function Stepper({ current }: { current: number }) {
|
||||
function Stepper({ current, labels = STEPS }: { current: number; labels?: string[] }) {
|
||||
return (
|
||||
<div className="steps">
|
||||
{STEPS.map((label, i) => {
|
||||
{labels.map((label, i) => {
|
||||
const done = i < current, on = i === current;
|
||||
return (
|
||||
<div key={label} style={{ display: "contents" }}>
|
||||
|
||||
Reference in New Issue
Block a user