feat: add user centric features
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* AuthGate — a global "require login before continuing" prompt.
|
||||
*
|
||||
* Why a gate at all? When an unauthenticated user clicks something that
|
||||
* needs them to be logged in (Bookmark, Save to collection, etc.) we want to
|
||||
*
|
||||
* 1. Show a login/signup modal *on the same page* (no redirect).
|
||||
* 2. Let them finish the original action right after they log in.
|
||||
* 3. Keep their URL — bookmarks, scroll position, selection — untouched.
|
||||
*
|
||||
* The trick is the imperative `requireAuth()` call: it returns a Promise that
|
||||
* resolves `true` once the user is authenticated, or `false` if they close
|
||||
* the modal without doing so. Calling code reads like a sync check:
|
||||
*
|
||||
* if (!(await requireAuth())) return;
|
||||
* // ... proceed with the protected API call
|
||||
*
|
||||
* Implementation sketch:
|
||||
* - We hold a single "pending" promise resolver in a ref so a follow-on
|
||||
* call to requireAuth (e.g. from another button) hooks onto the same
|
||||
* modal session.
|
||||
* - When AuthProvider reports a user (via useAuth) and the modal is open,
|
||||
* we resolve the promise(s) with `true` and close.
|
||||
* - When the modal closes without a user, we resolve with `false`.
|
||||
*/
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useAuth } from "./auth-provider";
|
||||
import { AuthModal } from "./auth-modal";
|
||||
|
||||
type AuthGateValue = {
|
||||
/** Returns true if the user is (or becomes) authenticated; false if they dismiss. */
|
||||
requireAuth: () => Promise<boolean>;
|
||||
/** Force-open the modal regardless of auth state — for explicit "Log in" buttons. */
|
||||
openModal: () => void;
|
||||
};
|
||||
|
||||
const AuthGateContext = createContext<AuthGateValue | null>(null);
|
||||
|
||||
export function AuthGateProvider({ children }: { children: React.ReactNode }) {
|
||||
const { user, loading } = useAuth();
|
||||
const [open, setOpen] = useState(false);
|
||||
const resolversRef = useRef<((v: boolean) => void)[]>([]);
|
||||
|
||||
const requireAuth = useCallback(async () => {
|
||||
// Fast path: already logged in.
|
||||
if (user) return true;
|
||||
setOpen(true);
|
||||
return new Promise<boolean>((resolve) => {
|
||||
resolversRef.current.push(resolve);
|
||||
});
|
||||
}, [user]);
|
||||
|
||||
const openModal = useCallback(() => {
|
||||
if (!user) setOpen(true);
|
||||
}, [user]);
|
||||
|
||||
// Auto-resolve pending promises the moment the user becomes authenticated.
|
||||
// This is what flips us from "modal open, waiting" to "modal closed, action
|
||||
// continues" without any extra wiring at the call site.
|
||||
useEffect(() => {
|
||||
if (user && (open || resolversRef.current.length > 0)) {
|
||||
const pending = resolversRef.current;
|
||||
resolversRef.current = [];
|
||||
setOpen(false);
|
||||
pending.forEach((r) => r(true));
|
||||
}
|
||||
}, [user, open]);
|
||||
|
||||
const onDismiss = useCallback(() => {
|
||||
const pending = resolversRef.current;
|
||||
resolversRef.current = [];
|
||||
setOpen(false);
|
||||
pending.forEach((r) => r(false));
|
||||
}, []);
|
||||
|
||||
const value = useMemo<AuthGateValue>(
|
||||
() => ({ requireAuth, openModal }),
|
||||
[requireAuth, openModal],
|
||||
);
|
||||
|
||||
return (
|
||||
<AuthGateContext.Provider value={value}>
|
||||
{children}
|
||||
{/* Render the modal only when we genuinely need it. `!user` guards
|
||||
against the brief window during which the AuthProvider hasn't
|
||||
finished hydrating; we'd rather wait than flash the modal. */}
|
||||
{open && !user && !loading ? <AuthModal onDismiss={onDismiss} /> : null}
|
||||
</AuthGateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuthGate(): AuthGateValue {
|
||||
const ctx = useContext(AuthGateContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useAuthGate must be used inside <AuthGateProvider>");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
Reference in New Issue
Block a user