feat: AppShell integration — auth, email/SMS OTP login, live multi-role team management #1

Merged
tanweer919 merged 3 commits from tanweer919/lynkeduppro-crm:feat/appshell-integration into goutamnextflow 2026-07-11 14:25:00 +00:00
Showing only changes of commit 339e2b007a - Show all commits
+47 -12
View File
@@ -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,12 +73,33 @@ 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("");
try { await register(finalEmail, pw); } // 1) Auth account — appshell → Supabase (SSO users already have a session).
catch { setSubmitErr("We couldn't create that account. The email may already be registered."); return; } if (!sso) {
try { await register(finalEmail, pw); }
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;