Skip to content

Commit 43c36be

Browse files
committed
feat: improve login error handling on /loading
1 parent 542e810 commit 43c36be

File tree

8 files changed

+245
-29
lines changed

8 files changed

+245
-29
lines changed

content/src/fieldConfig/login-support-page.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
{
22
"loginSupportPage": {
3+
"unlinkedAccount": "It looks like you haven’t linked your myNewJersey account.",
4+
"havingTrouble": "Having trouble logging in?",
5+
"logoutButtonText": "Log out",
6+
"logoutButtonTextUnlinked": "log out",
7+
"logoutAndTryAgain": "~~logoutButton~~ and try again.",
8+
"logoutAndTryAgainUnlinked": "Please ~~logoutButton~~ and try again.",
9+
"forMoreAssistance": "For more assistance, visit our [Login Issues Help page](support/login).",
310
"pageTitle": "Having trouble logging in to Business.NJ.gov?",
411
"lastUpdated": "Nov 5, 2025",
512
"multipleAccountsCallout": ":::miniCallout{ calloutType=\"informational\" }\n**If you have multiple myNewJersey accounts, use the email linked to your business.**\n:::",

web/decap-config/collections/13-support.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ collections:
2121
collapsed: false
2222
widget: object
2323
fields:
24+
- { label: "Unlinked Account", name: "unlinkedAccount", widget: "string" }
25+
- { label: "Having Trouble", name: "havingTrouble", widget: "string" }
26+
- { label: "Logout Button Text", name: "logoutButtonText", widget: "string" }
27+
- {
28+
label: "Logout Button Text Unlinked",
29+
name: "logoutButtonTextUnlinked",
30+
widget: "string",
31+
}
32+
- { label: "Logout And Try Again", name: "logoutAndTryAgain", widget: "string" }
33+
- {
34+
label: "Logout And Try Again Unlinked",
35+
name: "logoutAndTryAgainUnlinked",
36+
widget: "string",
37+
}
38+
- { label: "For More Assistance", name: "forMoreAssistance", widget: "string" }
2439
- { label: "Page Title", name: "pageTitle", widget: "string" }
2540
- { label: "Last Updated Date", name: "lastUpdated", widget: "string" }
2641
- {

web/src/components/LoadingPageComponent.tsx

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,76 @@
1+
import { Content } from "@/components/Content";
2+
import { UnStyledButton } from "@/components/njwds-extended/UnStyledButton";
13
import { PageSkeleton } from "@/components/njwds-layout/PageSkeleton";
24
import { SingleColumnContainer } from "@/components/njwds/SingleColumnContainer";
35
import { PageCircularIndicator } from "@/components/PageCircularIndicator";
4-
import { ReactElement } from "react";
6+
import { AuthContext } from "@/contexts/authContext";
7+
import { IsAuthenticated } from "@/lib/auth/AuthContext";
8+
import { configureAmplify, triggerSignOut } from "@/lib/auth/sessionHelper";
9+
import { useConfig } from "@/lib/data-hooks/useConfig";
10+
import { ReactElement, useContext } from "react";
11+
12+
interface Props {
13+
hasError?: boolean;
14+
isLinkingError?: boolean;
15+
}
16+
17+
export const LoadingPageComponent = ({
18+
hasError = false,
19+
isLinkingError = false,
20+
}: Props): ReactElement => {
21+
const { Config } = useConfig();
22+
const { state: authState } = useContext(AuthContext);
23+
24+
const isLoggedIn = authState.isAuthenticated === IsAuthenticated.TRUE;
25+
26+
const customComponents = {
27+
logoutButton: (
28+
<UnStyledButton
29+
onClick={async () => {
30+
configureAmplify();
31+
await triggerSignOut();
32+
}}
33+
className="logout-button-unstyled"
34+
>
35+
{isLinkingError
36+
? Config.loginSupportPage.logoutButtonTextUnlinked
37+
: Config.loginSupportPage.logoutButtonText}
38+
</UnStyledButton>
39+
),
40+
};
41+
42+
const renderErrorState = (): JSX.Element => {
43+
const titleMessage = isLinkingError
44+
? Config.loginSupportPage.unlinkedAccount
45+
: Config.loginSupportPage.havingTrouble;
46+
47+
const titleClassName = isLinkingError ? undefined : "text-bold";
48+
49+
return (
50+
<div className="margin-top-neg-4 text-center">
51+
<p className={titleClassName}>{titleMessage}</p>
52+
53+
{isLoggedIn && (
54+
<Content customComponents={customComponents}>
55+
{isLinkingError
56+
? Config.loginSupportPage.logoutAndTryAgainUnlinked
57+
: Config.loginSupportPage.logoutAndTryAgain}
58+
</Content>
59+
)}
60+
61+
<Content className="margin-y-3" showLaunchIcon={false}>
62+
{Config.loginSupportPage.forMoreAssistance}
63+
</Content>
64+
</div>
65+
);
66+
};
567

6-
export const LoadingPageComponent = (): ReactElement => {
768
return (
869
<PageSkeleton showNavBar logoOnly="NAVIGATOR_LOGO">
970
<main className="usa-section padding-top-0 desktop:padding-top-8" id="main">
1071
<SingleColumnContainer>
1172
<PageCircularIndicator />
73+
{hasError && renderErrorState()}
1274
</SingleColumnContainer>
1375
</main>
1476
</PageSkeleton>

web/src/lib/auth/signinHelper.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,10 @@ export const onGuestSignIn = async ({
151151
break;
152152
}
153153
case ROUTES.loading: {
154-
setRegistrationDimension("Began Onboarding");
155-
push(ROUTES.onboarding);
154+
if (!encounteredMyNjLinkingError) {
155+
setRegistrationDimension("Began Onboarding");
156+
push(ROUTES.onboarding);
157+
}
156158
break;
157159
}
158160
case ROUTES.login: {

web/src/pages/_app.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { IntercomScript } from "@/components/IntercomScript";
55
import { NeedsAccountModal } from "@/components/auth/NeedsAccountModal";
66

77
import { RegistrationStatusSnackbar } from "@/components/auth/RegistrationStatusSnackbar";
8+
import { RemoveBusinessModal } from "@/components/dashboard/RemoveBusinessModal";
89
import { AuthContext, initialState } from "@/contexts/authContext";
910
import { ContextualInfoContext } from "@/contexts/contextualInfoContext";
1011
import { IntercomContext } from "@/contexts/intercomContext";
1112
import { NeedsAccountContext } from "@/contexts/needsAccountContext";
13+
import { RemoveBusinessContext } from "@/contexts/removeBusinessContext";
1214
import { RoadmapContext } from "@/contexts/roadmapContext";
1315
import { UpdateQueueContext } from "@/contexts/updateQueueContext";
1416
import { UserDataErrorContext } from "@/contexts/userDataErrorContext";
@@ -40,8 +42,6 @@ import Script from "next/script";
4042
import { ReactElement, useEffect, useReducer, useState } from "react";
4143
import { SWRConfig } from "swr";
4244
import "../styles/main.scss";
43-
import { RemoveBusinessContext } from "@/contexts/removeBusinessContext";
44-
import { RemoveBusinessModal } from "@/components/dashboard/RemoveBusinessModal";
4545

4646
AuthContext.displayName = "Authentication";
4747
RoadmapContext.displayName = "Roadmap";
@@ -97,7 +97,7 @@ const App = ({ Component, pageProps }: AppProps): ReactElement => {
9797
event:
9898
| "signedIn"
9999
| "signUp"
100-
| "signOut"
100+
| "signedOut"
101101
| "signIn_failure"
102102
| "tokenRefresh"
103103
| "tokenRefresh_failure"
@@ -115,7 +115,7 @@ const App = ({ Component, pageProps }: AppProps): ReactElement => {
115115
break;
116116
case "signUp":
117117
break;
118-
case "signOut":
118+
case "signedOut":
119119
break;
120120
case "signIn_failure":
121121
console.error("user sign in failed");
@@ -128,7 +128,7 @@ const App = ({ Component, pageProps }: AppProps): ReactElement => {
128128
case "configured":
129129
break;
130130
default:
131-
console.error("unknown payload type");
131+
console.error(`unknown payload type: ${data.payload.event}`);
132132
break;
133133
}
134134
};

web/src/pages/loading.tsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { LoadingPageComponent } from "@/components/LoadingPageComponent";
22
import { AuthContext } from "@/contexts/authContext";
33
import { getActiveUser, triggerSignIn } from "@/lib/auth/sessionHelper";
4-
import { onGuestSignIn } from "@/lib/auth/signinHelper";
54
import { useUserData } from "@/lib/data-hooks/useUserData";
65
import { QUERIES, ROUTES } from "@/lib/domain-logic/routes";
76
import analytics from "@/lib/utils/analytics";
87
import { useMountEffectWhenDefined } from "@/lib/utils/helpers";
98
import { onboardingCompleted } from "@businessnjgovnavigator/shared/";
109
import { GetStaticPropsResult } from "next";
1110
import { useRouter } from "next/compat/router";
12-
import { ReactElement, useContext, useEffect } from "react";
11+
import { ReactElement, useContext, useEffect, useState } from "react";
1312

1413
export const signInSamlError = "Name+ID+value+was+not+found+in+SAML";
1514

@@ -18,6 +17,15 @@ const LoadingPage = (): ReactElement => {
1817
const router = useRouter();
1918
const { dispatch } = useContext(AuthContext);
2019
const loginPageEnabled = process.env.FEATURE_LOGIN_PAGE === "true";
20+
const [showError, setShowError] = useState(false);
21+
22+
useEffect(() => {
23+
const timeoutId = setTimeout(() => {
24+
setShowError(true);
25+
}, 5000);
26+
27+
return (): void => clearTimeout(timeoutId);
28+
}, []);
2129

2230
useEffect(() => {
2331
/**
@@ -28,22 +36,16 @@ const LoadingPage = (): ReactElement => {
2836
if (!router?.isReady) {
2937
return;
3038
}
31-
3239
if (router.query[QUERIES.code]) {
3340
getActiveUser().then((currentUser) => {
3441
dispatch({ type: "LOGIN", activeUser: currentUser });
3542
});
36-
} else if (router && router.asPath && router.asPath.includes(signInSamlError)) {
43+
} else if (router?.asPath?.includes(signInSamlError)) {
3744
analytics.event.landing_page.arrive.get_unlinked_myNJ_account();
38-
onGuestSignIn({
39-
push: router.push,
40-
pathname: router.pathname,
41-
dispatch,
42-
encounteredMyNjLinkingError: true,
43-
});
45+
setShowError(true);
4446
} else {
4547
if (loginPageEnabled) {
46-
router && router.push(ROUTES.login);
48+
router.push(ROUTES.login);
4749
} else {
4850
triggerSignIn();
4951
}
@@ -70,7 +72,12 @@ const LoadingPage = (): ReactElement => {
7072
}
7173
}, userData);
7274

73-
return <LoadingPageComponent />;
75+
return (
76+
<LoadingPageComponent
77+
hasError={showError}
78+
isLinkingError={router?.asPath?.includes(signInSamlError) ?? false}
79+
/>
80+
);
7481
};
7582

7683
export function getStaticProps(): GetStaticPropsResult<{ noAuth: boolean }> {
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { LoadingPageComponent } from "@/components/LoadingPageComponent";
2+
import { IsAuthenticated } from "@/lib/auth/AuthContext";
3+
import { withAuth } from "@/test/helpers/helpers-renderers";
4+
import { getMergedConfig } from "@businessnjgovnavigator/shared/contexts";
5+
import { render, screen } from "@testing-library/react";
6+
7+
const Config = getMergedConfig();
8+
9+
jest.mock("next/compat/router", () => ({ useRouter: jest.fn() }));
10+
11+
describe("LoadingPageComponent", () => {
12+
describe("when there are no errors", () => {
13+
it("shows only the loading indicator", () => {
14+
render(
15+
withAuth(<LoadingPageComponent hasError={false} />, {
16+
isAuthenticated: IsAuthenticated.TRUE,
17+
}),
18+
);
19+
20+
expect(screen.queryByText(Config.loginSupportPage.havingTrouble)).not.toBeInTheDocument();
21+
expect(screen.queryByText(Config.loginSupportPage.unlinkedAccount)).not.toBeInTheDocument();
22+
});
23+
});
24+
25+
describe("when there are errors", () => {
26+
describe("user is logged in and has a myNJ linking error", () => {
27+
it("shows unlinked account message, logout instructions, and help link", () => {
28+
render(
29+
withAuth(<LoadingPageComponent hasError={true} isLinkingError={true} />, {
30+
isAuthenticated: IsAuthenticated.TRUE,
31+
}),
32+
);
33+
34+
expect(screen.getByText(Config.loginSupportPage.unlinkedAccount)).toBeInTheDocument();
35+
36+
expect(
37+
screen.getByRole("button", { name: Config.loginSupportPage.logoutButtonTextUnlinked }),
38+
).toBeInTheDocument();
39+
expect(screen.getByText(/please/i)).toBeInTheDocument();
40+
expect(screen.getByText(/and try again/i)).toBeInTheDocument();
41+
42+
expect(screen.getByRole("link", { name: /login issues help page/i })).toBeInTheDocument();
43+
});
44+
45+
it("'unlinked account' message is not bold", () => {
46+
render(
47+
withAuth(<LoadingPageComponent hasError={true} isLinkingError={true} />, {
48+
isAuthenticated: IsAuthenticated.TRUE,
49+
}),
50+
);
51+
52+
const UnlinkedAccountMessage = screen.getByText(Config.loginSupportPage.unlinkedAccount);
53+
expect(UnlinkedAccountMessage).not.toHaveClass("text-bold");
54+
});
55+
});
56+
57+
describe("user is logged in and has no myNJ linking error", () => {
58+
it("shows bolded 'having trouble' message, logout instructions, and help link", () => {
59+
render(
60+
withAuth(<LoadingPageComponent hasError={true} isLinkingError={false} />, {
61+
isAuthenticated: IsAuthenticated.TRUE,
62+
}),
63+
);
64+
65+
const havingTroubleMessage = screen.getByText(Config.loginSupportPage.havingTrouble);
66+
expect(havingTroubleMessage).toBeInTheDocument();
67+
expect(havingTroubleMessage).toHaveClass("text-bold");
68+
69+
expect(
70+
screen.getByRole("button", { name: Config.loginSupportPage.logoutButtonText }),
71+
).toBeInTheDocument();
72+
expect(screen.getByText(/and try again/i)).toBeInTheDocument();
73+
74+
expect(screen.getByRole("link", { name: /login issues help page/i })).toBeInTheDocument();
75+
});
76+
77+
it("does not show the 'Please' text from the unlinked 'log out' message'", () => {
78+
render(
79+
withAuth(<LoadingPageComponent hasError={true} isLinkingError={false} />, {
80+
isAuthenticated: IsAuthenticated.TRUE,
81+
}),
82+
);
83+
84+
expect(screen.queryByText(/please/i)).not.toBeInTheDocument();
85+
});
86+
});
87+
88+
describe("user is logged out and has a myNJ linking error", () => {
89+
it("shows unlinked account message and help link, but no logout link", () => {
90+
render(
91+
withAuth(<LoadingPageComponent hasError={true} isLinkingError={true} />, {
92+
isAuthenticated: IsAuthenticated.FALSE,
93+
}),
94+
);
95+
96+
expect(screen.getByText(Config.loginSupportPage.unlinkedAccount)).toBeInTheDocument();
97+
98+
expect(
99+
screen.queryByRole("button", { name: Config.loginSupportPage.logoutButtonText }),
100+
).not.toBeInTheDocument();
101+
// "please" is only in the "logged in and unlinked" error copy
102+
expect(screen.queryByText(/please/i)).not.toBeInTheDocument();
103+
104+
expect(screen.getByRole("link", { name: /login issues help page/i })).toBeInTheDocument();
105+
});
106+
});
107+
108+
describe("user is logged out and has no myNJ linking error", () => {
109+
it("shows bolded 'having trouble' message and help link, but no logout link", () => {
110+
render(
111+
withAuth(<LoadingPageComponent hasError={true} isLinkingError={false} />, {
112+
isAuthenticated: IsAuthenticated.FALSE,
113+
}),
114+
);
115+
116+
const havingTroubleMessage = screen.getByText(Config.loginSupportPage.havingTrouble);
117+
expect(havingTroubleMessage).toBeInTheDocument();
118+
expect(havingTroubleMessage).toHaveClass("text-bold");
119+
120+
expect(
121+
screen.queryByRole("button", { name: Config.loginSupportPage.logoutButtonText }),
122+
).not.toBeInTheDocument();
123+
124+
expect(screen.getByRole("link", { name: /login issues help page/i })).toBeInTheDocument();
125+
});
126+
});
127+
});
128+
});

0 commit comments

Comments
 (0)