Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions apps/hash-frontend/src/pages/_app.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,28 @@ const App: FunctionComponent<AppProps> = ({
const awaitingEmailVerificationStatus =
!!authenticatedUser && !emailVerificationStatusKnown && !aal2Required;

/**
* A `redirectTo` that points at the page we're already on is a no-op we must
* ignore. `getInitialProps` re-runs on every navigation β€” including the
* same-URL `router.replace`s this effect performs β€” and `useRouter()` returns
* a fresh object each time, so honouring such a redirect spins forever in a
* `replace` -> `getInitialProps` -> `replace` loop (e.g. landing on `/` after
* accepting an org invite, where a stale `redirectTo: "/"` kept re-firing).
*/
const pendingRedirect =
!!redirectTo && redirectTo !== router.asPath ? redirectTo : undefined;

/**
* Handle client-side redirects that were determined in getInitialProps.
* On the server these are HTTP 307s; on the client getInitialProps returns
* a `redirectTo` prop instead, and this effect performs the navigation after
* the current route transition completes (avoiding NProgress stalls).
*/
useEffect(() => {
if (redirectTo) {
void router.replace(redirectTo);
if (pendingRedirect) {
void router.replace(pendingRedirect);
}
}, [redirectTo, router]);
}, [pendingRedirect, router]);

useEffect(() => {
setSentryUser({ authenticatedUser });
Expand Down Expand Up @@ -167,8 +178,15 @@ const App: FunctionComponent<AppProps> = ({
// router.query is empty during server-side rendering for pages that don’t use
// getServerSideProps. By showing app skeleton on the server, we avoid UI
// mismatches during rehydration and improve type-safety of param extraction.
// We also gate on `redirectTo` so the page doesn't flash before navigating.
if (ssr || !router.isReady || awaitingEmailVerificationStatus || redirectTo) {
// We also gate on a pending redirect so the page doesn't flash before
// navigating. A `redirectTo` matching the current path isn't pending (see
// `pendingRedirect`), so we render rather than stall on the loading state.
if (
ssr ||
!router.isReady ||
awaitingEmailVerificationStatus ||
pendingRedirect
) {
return <Suspense />; // Replace with app skeleton
}

Expand Down
15 changes: 13 additions & 2 deletions apps/hash-frontend/src/pages/index.page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useQuery } from "@apollo/client";
import { Stack } from "@mui/material";
import { useRouter } from "next/router";
import { useMemo } from "react";
import { useEffect, useMemo } from "react";

import { hasAccessToHashQuery } from "../graphql/queries/user.queries";
import { getLayoutWithSidebar } from "../shared/layout";
Expand Down Expand Up @@ -30,9 +30,20 @@ const Page: NextPageWithLayout = () => {
}
}, [authenticatedUser, hasAccessToHashResponse]);

const shouldCompleteSignup =
!authenticatedUser?.accountSignupComplete && !!hasAccessToHash;

/**
* Send users who have access but haven't finished signup to `/signup`.
*/
useEffect(() => {
if (shouldCompleteSignup) {
void push("/signup");
}
}, [shouldCompleteSignup, push]);

if (!authenticatedUser?.accountSignupComplete) {
if (hasAccessToHash) {
void push("/signup");
return null;
}

Expand Down
42 changes: 42 additions & 0 deletions apps/hash-frontend/src/pages/shared/auth-info-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import {
getOutgoingLinksForEntity,
getRoots,
intervalCompareWithInterval,
intervalForTimestamp,
} from "@blockprotocol/graph/stdlib";
import {
Expand Down Expand Up @@ -53,6 +54,29 @@ export const AuthInfoContext = createContext<AuthInfoContextValue | undefined>(
undefined,
);

/**
* Returns `true` if `candidate` contains a newer edition of the user (by
* transaction time) than `existing`.
*/
const subgraphHasNewerUser = (
candidate: Subgraph<EntityRootType<HashEntity>>,
existing: Subgraph<EntityRootType<HashEntity>>,
): boolean => {
const candidateUser = getRoots(candidate)[0];
const existingUser = getRoots(existing)[0];

if (!candidateUser || !existingUser) {
return true;
}
Comment thread
CiaranMn marked this conversation as resolved.

return (
intervalCompareWithInterval(
existingUser.metadata.temporalVersioning.transactionTime,
candidateUser.metadata.temporalVersioning.transactionTime,
) < 0
);
};

type AuthInfoProviderProps = {
initialAuthenticatedUserSubgraph?: Subgraph<EntityRootType<HashEntity>>;
children: ReactElement;
Expand All @@ -72,6 +96,24 @@ export const AuthInfoProvider: FunctionComponent<AuthInfoProviderProps> = ({
const [emailVerificationStatusKnown, setEmailVerificationStatusKnown] =
useState(false);

/**
* `getInitialProps` re-fetches the authenticated user on every navigation and
* passes it as `initialAuthenticatedUserSubgraph`. Adopt that server-provided
* data whenever it is newer than what the client currently holds.
*/
useEffect(() => {
if (!initialAuthenticatedUserSubgraph) {
return;
}

setAuthenticatedUserSubgraph((current) =>
!current ||
subgraphHasNewerUser(initialAuthenticatedUserSubgraph, current)
? initialAuthenticatedUserSubgraph
: current,
);
}, [initialAuthenticatedUserSubgraph]);

const userMemberOfLinks = useMemo(() => {
if (!authenticatedUserSubgraph) {
return undefined;
Expand Down
18 changes: 14 additions & 4 deletions apps/hash-frontend/src/pages/shared/workspace-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,17 @@ export const WorkspaceContextProvider: FunctionComponent<{
}
}, [activeWorkspaceWebId, updateActiveWorkspaceWebId, authenticatedUser]);

const workspaceContextValue = useMemo<WorkspaceContextValue>(() => {
const activeWorkspace =
const activeWorkspace = useMemo(
() =>
authenticatedUser && authenticatedUser.accountId === activeWorkspaceWebId
? authenticatedUser
: authenticatedUser?.memberOf.find(
({ org: { webId } }) => webId === activeWorkspaceWebId,
)?.org;
)?.org,
[authenticatedUser, activeWorkspaceWebId],
);

useEffect(() => {
/**
* If there is an `activeWorkspaceWebId` and an `authenticatedUser`, but
* `activeWorkspace` is not defined, reset `activeWorkspaceWebId` to the
Expand All @@ -98,15 +101,22 @@ export const WorkspaceContextProvider: FunctionComponent<{
(authenticatedUser.accountId as WebId),
);
}
}, [
activeWorkspace,
activeWorkspaceWebId,
authenticatedUser,
updateActiveWorkspaceWebId,
]);

const workspaceContextValue = useMemo<WorkspaceContextValue>(() => {
return {
activeWorkspace,
activeWorkspaceWebId,
updateActiveWorkspaceWebId,
refetchActiveWorkspace: () => refetch().then(() => undefined),
};
}, [
authenticatedUser,
activeWorkspace,
activeWorkspaceWebId,
updateActiveWorkspaceWebId,
refetch,
Expand Down
23 changes: 20 additions & 3 deletions apps/hash-frontend/src/pages/signup.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,16 @@ const SignupPage: NextPageWithLayout = () => {
refetchInvites();
await refetchAuthenticatedUser();
updateActiveWorkspaceWebId(invitation.org.webId);
void router.replace("/");
/**
* Hard navigation rather than `router.replace`: a client-side
* transition here can leave this component mounted on `/` (Next
* doesn't reliably re-resolve the route component on these
* `getInitialProps`-driven transitions). A full load guarantees a
* clean home render with fresh auth state.
*
* @todo sort out signup redirecting logic generally
*/
window.location.assign("/");
return;
}

Expand Down Expand Up @@ -368,7 +377,16 @@ const SignupPage: NextPageWithLayout = () => {
updateActiveWorkspaceWebId(invitation.org.webId);
}

void router.push("/");
/**
* Hard navigation rather than `router.push`: a client-side transition
* here can leave this component mounted on `/` (Next doesn't reliably
* re-resolve the route component on these `getInitialProps`-driven
* transitions), stranding the user on a loading state. A full load
* guarantees a clean home render with fresh auth state.
*
* @todo sort out signup redirecting logic generally
*/
window.location.assign("/");
},
[
acceptInvitationOnce,
Expand All @@ -378,7 +396,6 @@ const SignupPage: NextPageWithLayout = () => {
refetchAuthenticatedUser,
updateAuthenticatedUser,
updateActiveWorkspaceWebId,
router,
],
);

Expand Down
Loading