Skip to content

Commit ef78e62

Browse files
committed
configure redirect paths in the gadget provider and be consistent between signed in and signed out
1 parent fbb62fb commit ef78e62

File tree

6 files changed

+122
-22
lines changed

6 files changed

+122
-22
lines changed

packages/react/spec/components/auth/SignedInOrRedirect.spec.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,38 @@ describe("SignedInOrRedirect", () => {
4141
expect(mockAssign).toHaveBeenCalledWith("https://test-app.gadget.app/?redirectTo=%2F");
4242
});
4343

44+
test("redirects when signed out and a signInPath is provided in the auth context", () => {
45+
const component = (
46+
<h1>
47+
<SignedInOrRedirect>Hello, Jane!</SignedInOrRedirect>
48+
</h1>
49+
);
50+
51+
const { rerender } = render(component, { wrapper: MockClientWrapper(superAuthApi, undefined, { signInPath: "sign-in" }) });
52+
53+
expectMockSignedOutUser();
54+
rerender(component);
55+
56+
expect(mockAssign).toHaveBeenCalledTimes(1);
57+
expect(mockAssign).toHaveBeenCalledWith("https://test-app.gadget.app/sign-in?redirectTo=%2F");
58+
});
59+
60+
test("redirects when signed out and a signInPath is provided in the auth context and an override path is provided", () => {
61+
const component = (
62+
<h1>
63+
<SignedInOrRedirect path="custom-sign-in">Hello, Jane!</SignedInOrRedirect>
64+
</h1>
65+
);
66+
67+
const { rerender } = render(component, { wrapper: MockClientWrapper(superAuthApi, undefined, { signInPath: "sign-in" }) });
68+
69+
expectMockSignedOutUser();
70+
rerender(component);
71+
72+
expect(mockAssign).toHaveBeenCalledTimes(1);
73+
expect(mockAssign).toHaveBeenCalledWith("https://test-app.gadget.app/custom-sign-in?redirectTo=%2F");
74+
});
75+
4476
test("redirects when signed in but has no associated user", () => {
4577
const component = (
4678
<h1>

packages/react/spec/components/auth/SignedOutOrRedirect.spec.tsx

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe("SignedOutOrRedirect", () => {
1414
// @ts-expect-error mock
1515
delete window.location;
1616
// @ts-expect-error mock
17-
window.location = { assign: mockAssign, origin: "https://test-app.gadget.app", pathname: "/" };
17+
window.location = { assign: mockAssign, origin: "https://test-app.gadget.app", pathname: "/sign-in" };
1818
});
1919

2020
afterEach(() => {
@@ -28,7 +28,7 @@ describe("SignedOutOrRedirect", () => {
2828
test("redirects when signed in", () => {
2929
const component = (
3030
<h1>
31-
<SignedOutOrRedirect path="/signed-in">Hello, Jane!</SignedOutOrRedirect>
31+
<SignedOutOrRedirect>Hello, Jane!</SignedOutOrRedirect>
3232
</h1>
3333
);
3434

@@ -37,14 +37,69 @@ describe("SignedOutOrRedirect", () => {
3737
expectMockSignedInUser();
3838
rerender(component);
3939

40+
expect(mockAssign).toHaveBeenCalledTimes(1);
41+
expect(mockAssign).toHaveBeenCalledWith("https://test-app.gadget.app/");
42+
});
43+
44+
test("redirects when signed in and a redirectOnSuccessfulSignInPath has been provided", () => {
45+
const component = (
46+
<h1>
47+
<SignedOutOrRedirect>Hello, Jane!</SignedOutOrRedirect>
48+
</h1>
49+
);
50+
51+
const { rerender } = render(component, {
52+
wrapper: MockClientWrapper(superAuthApi, undefined, { redirectOnSuccessfulSignInPath: "/signed-in" }),
53+
});
54+
55+
expectMockSignedInUser();
56+
rerender(component);
57+
4058
expect(mockAssign).toHaveBeenCalledTimes(1);
4159
expect(mockAssign).toHaveBeenCalledWith("https://test-app.gadget.app/signed-in");
4260
});
4361

62+
test("redirects when signed in and a redirectOnSuccessfulSignInPath has been provided and has been overriden", () => {
63+
const component = (
64+
<h1>
65+
<SignedOutOrRedirect path="/my-custom-path">Hello, Jane!</SignedOutOrRedirect>
66+
</h1>
67+
);
68+
69+
const { rerender } = render(component, {
70+
wrapper: MockClientWrapper(superAuthApi, undefined, { redirectOnSuccessfulSignInPath: "/signed-in" }),
71+
});
72+
73+
expectMockSignedInUser();
74+
rerender(component);
75+
76+
expect(mockAssign).toHaveBeenCalledTimes(1);
77+
expect(mockAssign).toHaveBeenCalledWith("https://test-app.gadget.app/my-custom-path");
78+
});
79+
80+
test("redirects after signing in to the redirect path", () => {
81+
window.location.search = "?redirectTo=%2Fredirect-me";
82+
const component = (
83+
<h1>
84+
<SignedOutOrRedirect>Hello, Jane!</SignedOutOrRedirect>
85+
</h1>
86+
);
87+
88+
const { rerender } = render(component, {
89+
wrapper: MockClientWrapper(superAuthApi, undefined, { redirectOnSuccessfulSignInPath: "/signed-in" }),
90+
});
91+
92+
expectMockSignedInUser();
93+
rerender(component);
94+
95+
expect(mockAssign).toHaveBeenCalledTimes(1);
96+
expect(mockAssign).toHaveBeenCalledWith("https://test-app.gadget.app/redirect-me");
97+
});
98+
4499
test("renders when signed out", () => {
45100
const component = (
46101
<h1>
47-
<SignedOutOrRedirect path="/signed-in">Hello, Jane!</SignedOutOrRedirect>
102+
<SignedOutOrRedirect>Hello, Jane!</SignedOutOrRedirect>
48103
</h1>
49104
);
50105

packages/react/spec/testWrappers.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,26 @@ import type { ReactNode } from "react";
33
import React, { Suspense } from "react";
44
import type { MockUrqlClient } from "../../api-client-core/spec/mockUrqlClient.js";
55
import { createMockUrqlClient, mockGraphQLWSClient, mockUrqlClient } from "../../api-client-core/spec/mockUrqlClient.js";
6-
import { Provider } from "../src/GadgetProvider.js";
6+
import { Provider, type GadgetAuthConfiguration } from "../src/GadgetProvider.js";
77

8-
export const MockClientWrapper = (api: AnyClient, urqlClient?: MockUrqlClient) => (props: { children: ReactNode }) => {
9-
const urql = urqlClient ?? mockUrqlClient;
8+
export const MockClientWrapper =
9+
(api: AnyClient, urqlClient?: MockUrqlClient, auth?: Partial<GadgetAuthConfiguration>) => (props: { children: ReactNode }) => {
10+
const urql = urqlClient ?? mockUrqlClient;
1011

11-
jest.spyOn(api.connection, "currentClient", "get").mockReturnValue(urql);
12+
jest.spyOn(api.connection, "currentClient", "get").mockReturnValue(urql);
1213

13-
return (
14-
<Provider api={api}>
15-
<Suspense fallback={<div>Loading...</div>}>{props.children}</Suspense>
16-
</Provider>
17-
);
18-
};
14+
return (
15+
<Provider api={api} auth={auth}>
16+
<Suspense fallback={<div>Loading...</div>}>{props.children}</Suspense>
17+
</Provider>
18+
);
19+
};
1920

20-
export const MockGraphQLWSClientWrapper = (api: AnyClient) => (props: { children: ReactNode }) => {
21+
export const MockGraphQLWSClientWrapper = (api: AnyClient, auth?: Partial<GadgetAuthConfiguration>) => (props: { children: ReactNode }) => {
2122
jest.replaceProperty(api.connection, "baseSubscriptionClient", mockGraphQLWSClient as any);
2223

2324
return (
24-
<Provider api={api}>
25+
<Provider api={api} auth={auth}>
2526
<Suspense fallback={<div>Loading...</div>}>{props.children}</Suspense>
2627
</Provider>
2728
);

packages/react/src/GadgetProvider.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export interface GadgetAuthConfiguration {
1818
signInPath: string;
1919
/** The API identifier of the `User` `signOut` action. Defaults to `signOut` */
2020
signOutActionApiIdentifier: string;
21+
/** The path that users are redirected to after they sign in successfully. */
22+
redirectOnSuccessfulSignInPath: string;
2123
}
2224

2325
/** Provides the api client instance, if present, as well as the Gadget auth configuration for the application. */
@@ -61,6 +63,7 @@ export interface DeprecatedProviderProps {
6163

6264
const defaultSignInPath = "/";
6365
const defaultSignOutApiIdentifier = "signOut";
66+
const defaultRedirectOnSuccessfulSignInPath = "/";
6467

6568
/**
6669
* Provider wrapper component that passes an api client instance to the other hooks.
@@ -109,11 +112,13 @@ export function Provider(props: ProviderProps | DeprecatedProviderProps) {
109112

110113
let signInPath = defaultSignInPath;
111114
let signOutActionApiIdentifier = defaultSignOutApiIdentifier;
115+
let redirectOnSuccessfulSignInPath = defaultRedirectOnSuccessfulSignInPath;
112116

113117
if ("auth" in props) {
114118
const { auth } = props;
115119
if (auth?.signInPath) signInPath = auth.signInPath;
116120
if (auth?.signOutActionApiIdentifier) signOutActionApiIdentifier = auth.signOutActionApiIdentifier;
121+
if (auth?.redirectOnSuccessfulSignInPath) redirectOnSuccessfulSignInPath = auth.redirectOnSuccessfulSignInPath;
117122
}
118123

119124
return (
@@ -124,6 +129,7 @@ export function Provider(props: ProviderProps | DeprecatedProviderProps) {
124129
auth: {
125130
signInPath,
126131
signOutActionApiIdentifier,
132+
redirectOnSuccessfulSignInPath,
127133
},
128134
}}
129135
>

packages/react/src/auth/SignedInOrRedirect.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useAuth } from "./useAuth.js";
66
/**
77
* Renders its `children` if the current `Session` is signed in, otherwise redirects the browser to the `signInPath` configured in the `Provider`. Uses `window.location.assign` to perform the redirect.
88
*/
9-
export const SignedInOrRedirect = (props: { children: ReactNode }) => {
9+
export const SignedInOrRedirect = (props: { path?: string; children: ReactNode }) => {
1010
const [redirected, setRedirected] = useState(false);
1111

1212
const { user, isSignedIn } = useAuth();
@@ -16,11 +16,12 @@ export const SignedInOrRedirect = (props: { children: ReactNode }) => {
1616
useEffect(() => {
1717
if (auth && !redirected && (!isSignedIn || !user)) {
1818
setRedirected(true);
19-
const redirectUrl = new URL(auth.signInPath, window.location.origin);
19+
const redirectPath = props.path ?? auth.signInPath;
20+
const redirectUrl = new URL(redirectPath, window.location.origin);
2021
redirectUrl.searchParams.set("redirectTo", window.location.pathname);
2122
window.location.assign(redirectUrl.toString());
2223
}
23-
}, [redirected, isSignedIn, auth]);
24+
}, [props.path, redirected, isSignedIn, auth, user]);
2425

2526
if (user && isSignedIn) {
2627
return <>{props.children}</>;

packages/react/src/auth/SignedOutOrRedirect.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
import type { ReactNode } from "react";
2-
import React, { useEffect, useState } from "react";
2+
import React, { useContext, useEffect, useState } from "react";
3+
import { GadgetConfigurationContext } from "../GadgetProvider.js";
34
import { useAuth } from "./useAuth.js";
45

56
/**
67
* Renders its `children` if the current `Session` is signed out, otherwise redirects the browser to the `path` prop. Uses `window.location.assign` to perform the redirect.
78
*/
8-
export const SignedOutOrRedirect = (props: { path: string; children: ReactNode }) => {
9+
export const SignedOutOrRedirect = (props: { path?: string; children: ReactNode }) => {
910
const [redirected, setRedirected] = useState(false);
1011
const { path, children } = props;
1112

1213
const { user, isSignedIn } = useAuth();
14+
const context = useContext(GadgetConfigurationContext);
15+
const { auth } = context ?? {};
1316

1417
useEffect(() => {
1518
if (!redirected && (isSignedIn || user)) {
1619
setRedirected(true);
17-
const redirectUrl = new URL(path, window.location.origin);
20+
const searchParams = new URLSearchParams(window.location.search);
21+
const redirectPath = searchParams.get("redirectTo") ?? path ?? auth?.redirectOnSuccessfulSignInPath ?? "/";
22+
const redirectUrl = new URL(redirectPath, window.location.origin);
1823
window.location.assign(redirectUrl.toString());
1924
}
20-
}, [redirected, isSignedIn, path, user]);
25+
}, [redirected, isSignedIn, path, user, auth]);
2126

2227
if (!user && !isSignedIn) {
2328
return <>{children}</>;

0 commit comments

Comments
 (0)