feat: AppShell integration — auth, email/SMS OTP login, live multi-role team management #1
@@ -11,18 +11,26 @@ import {
|
|||||||
countryCodes, relationshipOptions, addressCountries,
|
countryCodes, relationshipOptions, addressCountries,
|
||||||
TERMS, PRIVACY, passwordStrength, type AddrCountry,
|
TERMS, PRIVACY, passwordStrength, type AddrCountry,
|
||||||
} from "./data";
|
} from "./data";
|
||||||
import { useAuth } 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";
|
||||||
|
|
||||||
|
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@]+$/;
|
||||||
|
|
||||||
export function RegisterFlow() {
|
export function RegisterFlow() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { register } = useAuth();
|
const { register } = useAuth();
|
||||||
|
const { sdk } = useAppShell();
|
||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
const [submitErr, setSubmitErr] = useState("");
|
const [submitErr, setSubmitErr] = useState("");
|
||||||
|
|
||||||
|
// address (lifted from StepAddress/AddressBlock so finish() can persist it)
|
||||||
|
const [regAddr, setRegAddr] = useState<Addr>({});
|
||||||
|
const [mailAddr, setMailAddr] = useState<Addr>({});
|
||||||
|
const [mailingSame, setMailingSame] = useState(true);
|
||||||
|
|
||||||
// shared form state
|
// shared form state
|
||||||
const [sso, setSso] = useState<{ provider: string; email: string } | null>(null);
|
const [sso, setSso] = useState<{ provider: string; email: string } | null>(null);
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
@@ -65,13 +73,34 @@ export function RegisterFlow() {
|
|||||||
allotteeNames: isSelf ? null : `${alloeFirst} ${alloeLast}`.trim(),
|
allotteeNames: isSelf ? null : `${alloeFirst} ${alloeLast}`.trim(),
|
||||||
};
|
};
|
||||||
try { localStorage.setItem("lup_profile", JSON.stringify(profile)); } catch { /* ignore */ }
|
try { localStorage.setItem("lup_profile", JSON.stringify(profile)); } catch { /* ignore */ }
|
||||||
// With the Shell live, create the real account through appshell → BFF. SSO users
|
|
||||||
// already have a session from OAuth, so only email/password sign-ups register here.
|
if (isShellConfigured()) {
|
||||||
if (isShellConfigured() && !sso) {
|
|
||||||
setSubmitErr("");
|
setSubmitErr("");
|
||||||
|
// 1) Auth account — appshell → Supabase (SSO users already have a session).
|
||||||
|
if (!sso) {
|
||||||
try { await register(finalEmail, pw); }
|
try { await register(finalEmail, pw); }
|
||||||
catch { setSubmitErr("We couldn't create that account. The email may already be registered."); return; }
|
catch { setSubmitErr("We couldn't create that account. The email may already be registered."); return; }
|
||||||
}
|
}
|
||||||
|
// 2) Persist the full CRM registration payload to be-crm (name, phone, persona,
|
||||||
|
// allottee, both addresses, consent). Non-fatal: the auth account exists either way.
|
||||||
|
try {
|
||||||
|
const mailing = mailingSame ? regAddr : mailAddr;
|
||||||
|
await sdk.command("crm.account.register", {
|
||||||
|
email: finalEmail,
|
||||||
|
firstName: first, lastName: last,
|
||||||
|
phoneCc: cc, phoneNumber: phone.replace(/\D/g, ""),
|
||||||
|
persona: relationship,
|
||||||
|
isAllottee: isSelf,
|
||||||
|
...(isSelf ? {} : { allotteeId: alloeNo, allotteeFirstName: alloeFirst, allotteeLastName: alloeLast }),
|
||||||
|
registeredAddress: regAddr,
|
||||||
|
mailingAddress: mailing,
|
||||||
|
mailingSameAsRegistered: mailingSame,
|
||||||
|
consentTerms: termsOk, consentPrivacy: privacyOk,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("crm.account.register failed (continuing):", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
router.push("/dashboard");
|
router.push("/dashboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +135,7 @@ export function RegisterFlow() {
|
|||||||
{step === 2 && (
|
{step === 2 && (
|
||||||
<>
|
<>
|
||||||
{submitErr && <div style={{ marginBottom: 14 }}><FlashNote tone="error">{submitErr}</FlashNote></div>}
|
{submitErr && <div style={{ marginBottom: 14 }}><FlashNote tone="error">{submitErr}</FlashNote></div>}
|
||||||
<StepAddress onBack={() => setStep(1)} onFinish={finish} />
|
<StepAddress sameAs={mailingSame} setSameAs={setMailingSame} onRegAddr={setRegAddr} onMailAddr={setMailAddr} onBack={() => setStep(1)} onFinish={finish} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -391,8 +420,7 @@ function VerifyChannel({ kind, initial, country, cc, initialPhone, verified, onV
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ====================== STEP 3 — ADDRESS ====================== */
|
/* ====================== STEP 3 — ADDRESS ====================== */
|
||||||
function StepAddress({ onBack, onFinish }: { onBack: () => void; onFinish: () => void }) {
|
function StepAddress({ sameAs, setSameAs, onRegAddr, onMailAddr, onBack, onFinish }: { sameAs: boolean; setSameAs: (v: boolean) => void; onRegAddr: (a: Addr) => void; onMailAddr: (a: Addr) => void; onBack: () => void; onFinish: () => void }) {
|
||||||
const [sameAs, setSameAs] = useState(true);
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<StepBack onClick={onBack} />
|
<StepBack onClick={onBack} />
|
||||||
@@ -400,7 +428,7 @@ function StepAddress({ onBack, onFinish }: { onBack: () => void; onFinish: () =>
|
|||||||
<p className="sub">Add your registered address and where we should send mail.</p>
|
<p className="sub">Add your registered address and where we should send mail.</p>
|
||||||
|
|
||||||
<AddrSectionHead icon="home" tone="amber" title="Registered address" sub="Where your property is registered" />
|
<AddrSectionHead icon="home" tone="amber" title="Registered address" sub="Where your property is registered" />
|
||||||
<AddressBlock idPrefix="reg" />
|
<AddressBlock idPrefix="reg" onChange={onRegAddr} />
|
||||||
|
|
||||||
<label className="check-row" style={{ marginTop: 16 }}>
|
<label className="check-row" style={{ marginTop: 16 }}>
|
||||||
<input type="checkbox" checked={sameAs} onChange={(e) => setSameAs(e.target.checked)} />
|
<input type="checkbox" checked={sameAs} onChange={(e) => setSameAs(e.target.checked)} />
|
||||||
@@ -410,7 +438,7 @@ function StepAddress({ onBack, onFinish }: { onBack: () => void; onFinish: () =>
|
|||||||
{!sameAs && (
|
{!sameAs && (
|
||||||
<>
|
<>
|
||||||
<AddrSectionHead icon="mail" tone="blue" title="Mailing / correspondence address" sub="Where we send physical mail" />
|
<AddrSectionHead icon="mail" tone="blue" title="Mailing / correspondence address" sub="Where we send physical mail" />
|
||||||
<AddressBlock idPrefix="mail" />
|
<AddressBlock idPrefix="mail" onChange={onMailAddr} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -435,7 +463,7 @@ function AddrSectionHead({ icon, title, sub, tone }: { icon: string; title: stri
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddressBlock({ idPrefix }: { idPrefix: string }) {
|
function AddressBlock({ idPrefix, onChange }: { idPrefix: string; onChange?: (a: Addr) => void }) {
|
||||||
const [country, setCountry] = useState<AddrCountry>(addressCountries[0]);
|
const [country, setCountry] = useState<AddrCountry>(addressCountries[0]);
|
||||||
const [pin, setPin] = useState("");
|
const [pin, setPin] = useState("");
|
||||||
const [city, setCity] = useState("");
|
const [city, setCity] = useState("");
|
||||||
@@ -448,6 +476,13 @@ function AddressBlock({ idPrefix }: { idPrefix: string }) {
|
|||||||
const [suggestions, setSuggestions] = useState<{ line1: string; line2: string; city: string; state: string; postal: string }[]>([]);
|
const [suggestions, setSuggestions] = useState<{ line1: string; line2: string; city: string; state: string; postal: string }[]>([]);
|
||||||
const debounce = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const debounce = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
// Report the composed address up so the parent can persist it (postalCode is the
|
||||||
|
// PIN for India, the postal field elsewhere).
|
||||||
|
useEffect(() => {
|
||||||
|
onChange?.({ line1, line2, city, state: stateName, postalCode: postal || pin, country: country.code, locality });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [line1, line2, city, stateName, postal, pin, country, locality]);
|
||||||
|
|
||||||
// India: PIN auto-fill
|
// India: PIN auto-fill
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (country.lookup !== "pincode" || pin.length !== 6) return;
|
if (country.lookup !== "pincode" || pin.length !== 6) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user