Skip to content

Commit 433e98e

Browse files
committed
Refactor
1 parent 12a36d4 commit 433e98e

File tree

6 files changed

+156
-97
lines changed

6 files changed

+156
-97
lines changed

src/client.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { type createClient, LoginRequiredError } from "@workos-inc/authkit-js";
2+
3+
export type Client = Pick<
4+
Awaited<ReturnType<typeof createClient>>,
5+
| "signIn"
6+
| "signUp"
7+
| "getUser"
8+
| "getAccessToken"
9+
| "signOut"
10+
| "switchToOrganization"
11+
>;
12+
13+
export type CreateClientOptions = NonNullable<
14+
Parameters<typeof createClient>[1]
15+
>;
16+
17+
export const NOOP_CLIENT: Client = {
18+
signIn: async () => {},
19+
signUp: async () => {},
20+
getUser: () => null,
21+
getAccessToken: () => Promise.reject(new LoginRequiredError()),
22+
switchToOrganization: () => Promise.resolve(),
23+
signOut: async () => {},
24+
};

src/context.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
"use client";
22

33
import * as React from "react";
4-
import { Client } from "./types";
5-
import { State, initialState } from "./state";
4+
import type { Client } from "./client";
5+
import type { State } from "./state";
66

7-
export interface ContextValue extends Client, State {
8-
accessToken: string | null;
7+
export interface AuthKitContextValue {
8+
client: Client;
9+
state: State;
910
}
1011

11-
export const Context = React.createContext<ContextValue>({
12-
...initialState,
13-
signIn: () => Promise.reject(),
14-
signUp: () => Promise.reject(),
15-
getUser: () => null,
16-
getAccessToken: () => Promise.reject(),
17-
signOut: () => Promise.reject(),
18-
switchToOrganization: () => Promise.reject(),
19-
accessToken: null,
20-
});
12+
export const AuthKitContext = React.createContext<AuthKitContextValue | null>(
13+
null,
14+
);
15+
AuthKitContext.displayName = "AuthKitContext";

src/hook.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import * as React from "react";
2-
import { Context } from "./context";
3-
import { initialState } from "./state";
2+
import type { State } from "./state";
3+
import type { Client } from "./client";
4+
import { AuthKitContext } from "./context";
45

5-
export function useAuth() {
6-
const context = React.useContext(Context);
7-
8-
if (context === initialState) {
6+
export function useAuth(): Client & State {
7+
const context = React.useContext(AuthKitContext);
8+
if (context === null) {
99
throw new Error("useAuth must be used within an AuthKitProvider");
1010
}
1111

12-
return context;
12+
return React.useMemo(
13+
() => ({ ...context.client, ...context.state }),
14+
[context.client, context.state],
15+
);
1316
}

src/provider.tsx

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {
77
LoginRequiredError,
88
OnRefreshResponse,
99
} from "@workos-inc/authkit-js";
10-
import { Context } from "./context";
11-
import { Client, CreateClientOptions } from "./types";
12-
import { initialState } from "./state";
10+
import { AuthKitContext } from "./context";
11+
import type { Client, CreateClientOptions } from "./client";
12+
import { INITIAL_STATE, LOADING_STATE, State } from "./state";
1313

1414
interface AuthKitProviderProps extends CreateClientOptions {
1515
clientId: string;
@@ -31,7 +31,7 @@ export function AuthKitProvider(props: AuthKitProviderProps) {
3131
refreshBufferInterval,
3232
} = props;
3333
const [client, setClient] = React.useState<Client>(NOOP_CLIENT);
34-
const [state, setState] = React.useState(initialState);
34+
const [state, setState] = React.useState<State>(INITIAL_STATE);
3535

3636
const handleRefresh = React.useCallback(
3737
(response: OnRefreshResponse) => {
@@ -49,15 +49,17 @@ export function AuthKitProvider(props: AuthKitProviderProps) {
4949
} = getClaims(accessToken);
5050
setState((prev) => {
5151
const next = {
52-
...prev,
52+
status: "authenticated-refreshed",
53+
isLoading: false,
5354
user,
5455
organizationId,
5556
role,
5657
roles,
5758
permissions,
5859
featureFlags,
5960
impersonator,
60-
};
61+
accessToken,
62+
} satisfies State;
6163
return isEquivalentWorkOSSession(prev, next) ? prev : next;
6264
});
6365
onRefresh?.(response);
@@ -66,8 +68,9 @@ export function AuthKitProvider(props: AuthKitProviderProps) {
6668
);
6769

6870
React.useEffect(() => {
71+
let isCurrentRun = true;
6972
function initialize() {
70-
const timeoutId = setTimeout(() => {
73+
const timeoutId = window.setTimeout(() => {
7174
createClient(clientId, {
7275
apiHostname,
7376
port,
@@ -78,7 +81,11 @@ export function AuthKitProvider(props: AuthKitProviderProps) {
7881
onRefresh: handleRefresh,
7982
onRefreshFailure,
8083
refreshBufferInterval,
81-
}).then(async (client) => {
84+
}).then((client) => {
85+
if (!isCurrentRun) {
86+
return;
87+
}
88+
8289
const user = client.getUser();
8390
setClient({
8491
getAccessToken: client.getAccessToken.bind(client),
@@ -88,54 +95,48 @@ export function AuthKitProvider(props: AuthKitProviderProps) {
8895
signOut: client.signOut.bind(client),
8996
switchToOrganization: client.switchToOrganization.bind(client),
9097
});
91-
setState((prev) => ({ ...prev, isLoading: false, user }));
98+
setState((prev) =>
99+
user
100+
? {
101+
...prev,
102+
status: "authenticated",
103+
isLoading: false,
104+
user,
105+
}
106+
: {
107+
...prev,
108+
status: "unauthenticated",
109+
isLoading: false,
110+
user: null,
111+
accessToken: null,
112+
},
113+
);
92114
});
93115
});
94116

95117
return () => {
96-
clearTimeout(timeoutId);
118+
isCurrentRun = false;
119+
window.clearTimeout(timeoutId);
97120
};
98121
}
99122

100123
setClient(NOOP_CLIENT);
101-
setState(initialState);
124+
setState(LOADING_STATE);
102125

103126
return initialize();
104127
}, [clientId, apiHostname, https, port, redirectUri, refreshBufferInterval]);
105128

106-
const [accessToken, setAccessToken] = React.useState<string | null>(null);
107-
React.useEffect(() => {
108-
const handleAccessTokenChange = (
109-
event: CustomEvent<{ accessToken: string }>,
110-
) => {
111-
setAccessToken(event.detail.accessToken);
112-
};
113-
114-
// authkit-js emits a "authkit:tokenchange" event when the access token is
115-
// refreshed. We want to use this to update the state with the new access
116-
// token so that it is available and up-to-date at render-time.
117-
window.addEventListener("authkit:tokenchange", handleAccessTokenChange);
118-
return () => {
119-
window.removeEventListener(
120-
"authkit:tokenchange",
121-
handleAccessTokenChange,
122-
);
123-
};
124-
}, []);
125-
126129
return (
127-
<Context.Provider value={{ ...client, ...state, accessToken }}>
130+
<AuthKitContext.Provider value={{ client, state }}>
128131
{children}
129-
</Context.Provider>
132+
</AuthKitContext.Provider>
130133
);
131134
}
132135

133136
// poor-man's "deep equality" check
134-
function isEquivalentWorkOSSession(
135-
a: typeof initialState,
136-
b: typeof initialState,
137-
) {
137+
function isEquivalentWorkOSSession(a: State, b: State) {
138138
return (
139+
a.status === b.status &&
139140
a.user?.updatedAt === b.user?.updatedAt &&
140141
a.organizationId === b.organizationId &&
141142
a.role === b.role &&
@@ -145,7 +146,8 @@ function isEquivalentWorkOSSession(
145146
a.featureFlags.length === b.featureFlags.length &&
146147
a.featureFlags.every((flag, i) => flag === b.featureFlags[i]) &&
147148
a.impersonator?.email === b.impersonator?.email &&
148-
a.impersonator?.reason === b.impersonator?.reason
149+
a.impersonator?.reason === b.impersonator?.reason &&
150+
a.accessToken === b.accessToken
149151
);
150152
}
151153

src/state.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,82 @@ export interface Impersonator {
55
reason: string | null;
66
}
77

8-
export interface State {
9-
isLoading: boolean;
8+
type Status =
9+
| "initial"
10+
| "loading"
11+
| "authenticated"
12+
| "authenticated-refreshed"
13+
| "unauthenticated";
14+
15+
interface SharedState {
16+
status: Status;
1017
user: User | null;
1118
role: string | null;
1219
roles: string[] | null;
1320
organizationId: string | null;
1421
permissions: string[];
1522
featureFlags: string[];
1623
impersonator: Impersonator | null;
24+
accessToken: string | null;
25+
}
26+
27+
interface InitialState extends SharedState {
28+
status: "initial";
29+
isLoading: true;
30+
user: null;
31+
role: null;
32+
roles: null;
33+
organizationId: null;
34+
permissions: [];
35+
featureFlags: [];
36+
impersonator: null;
37+
accessToken: null;
38+
}
39+
40+
interface LoadingState extends SharedState {
41+
status: "loading";
42+
isLoading: true;
43+
user: null;
44+
role: null;
45+
roles: null;
46+
organizationId: null;
47+
permissions: [];
48+
featureFlags: [];
49+
impersonator: null;
50+
accessToken: null;
51+
}
52+
53+
interface AuthenticatedState extends SharedState {
54+
status: "authenticated";
55+
isLoading: false;
56+
user: User;
57+
accessToken: string | null;
1758
}
1859

19-
export const initialState: State = {
60+
interface AuthenticatedRefreshedState extends SharedState {
61+
status: "authenticated-refreshed";
62+
isLoading: false;
63+
user: User;
64+
accessToken: string;
65+
}
66+
67+
interface UnauthenticatedState extends SharedState {
68+
status: "unauthenticated";
69+
isLoading: false;
70+
user: null;
71+
accessToken: null;
72+
}
73+
74+
// TODO: Add error states
75+
export type State =
76+
| InitialState
77+
| LoadingState
78+
| AuthenticatedState
79+
| AuthenticatedRefreshedState
80+
| UnauthenticatedState;
81+
82+
export const INITIAL_STATE: InitialState = {
83+
status: "initial",
2084
isLoading: true,
2185
user: null,
2286
role: null,
@@ -25,4 +89,10 @@ export const initialState: State = {
2589
permissions: [],
2690
featureFlags: [],
2791
impersonator: null,
92+
accessToken: null,
93+
};
94+
95+
export const LOADING_STATE: LoadingState = {
96+
...INITIAL_STATE,
97+
status: "loading",
2898
};

src/types.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.

0 commit comments

Comments
 (0)