diff --git a/lib/auth-context.tsx b/lib/auth-context.tsx new file mode 100644 index 0000000..9a24076 --- /dev/null +++ b/lib/auth-context.tsx @@ -0,0 +1,38 @@ +import React from "react"; + +/** + * Generic interface for authentication. + */ +export interface AuthContext { + /** + * The currently logged-in user. This will be + * null if the user isn't logged in, otherwise it will + * be their name. + */ + loggedInUser: string | null; + + /** + * The name of the authentication provider, e.g. "GitHub", + * or null if auth is disabled. + */ + providerName: string | null; + + /** + * If authentication failed for some reason, this will + * be a string describing the error. + */ + error?: string; + + /** Begin the login UI flow. */ + login(): void; + + /** Log out the user. */ + logout(): void; +} + +export const AuthContext = React.createContext({ + loggedInUser: null, + providerName: null, + login() {}, + logout() {}, +}); diff --git a/lib/auth.tsx b/lib/auth.tsx deleted file mode 100644 index 1390e05..0000000 --- a/lib/auth.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { FirebaseApp, FirebaseOptions, initializeApp } from "firebase/app"; -import { - getAuth, - signInWithPopup, - GithubAuthProvider, - onAuthStateChanged, - signOut, - Auth, - User, -} from "firebase/auth"; -import React, { useCallback, useEffect, useState } from "react"; - -const DEFAULT_APP_CONFIG: FirebaseOptions = { - apiKey: "AIzaSyAV1kkVvSKEicEa8rLke9o_BxYBu1rb8kw", - authDomain: "mystic-addaf.firebaseapp.com", - projectId: "mystic-addaf", - storageBucket: "mystic-addaf.appspot.com", - messagingSenderId: "26787182745", - appId: "1:26787182745:web:e4fbd9439b9279fe966008", - measurementId: "G-JHKRSK1PR6", -}; - -/** - * Generic interface for authentication. - */ -export interface AuthContext { - /** - * The currently logged-in user. This will be - * null if the user isn't logged in, otherwise it will - * be their name. - */ - loggedInUser: string | null; - - /** - * The name of the authentication provider, e.g. "GitHub", - * or null if auth is disabled. - */ - providerName: string | null; - - /** - * If authentication failed for some reason, this will - * be a string describing the error. - */ - error?: string; - - /** Begin the login UI flow. */ - login(): void; - - /** Log out the user. */ - logout(): void; -} - -type FirebaseGithubAuthState = { - app: FirebaseApp; - auth: Auth; - provider: GithubAuthProvider; -}; - -/** - * A Firebase GitHub authentication provider. - * - * Note this component is assumed to never be unmounted, nor - * for its props to change. - */ -export const FirebaseGithubAuthProvider: React.FC<{ - config?: FirebaseOptions; -}> = ({ config, children }) => { - const [state, setState] = useState(null); - const [user, setUser] = useState(null); - const [error, setError] = useState(undefined); - - const handleError = (e: Error) => setError(e.message); - - useEffect(() => { - const app = initializeApp(config || DEFAULT_APP_CONFIG); - const auth = getAuth(app); - const provider = new GithubAuthProvider(); - - setState({ app, auth, provider }); - onAuthStateChanged(auth, setUser); - }, [config]); - - const context: AuthContext = { - loggedInUser: user && user.displayName, - providerName: "GitHub", - error, - login: useCallback(() => { - setError(undefined); - state && signInWithPopup(state.auth, state.provider).catch(handleError); - }, [state]), - logout: useCallback(() => { - setError(undefined); - state && signOut(state.auth).catch(handleError); - }, [state]), - }; - - return ; -}; - -export const AuthContext = React.createContext({ - loggedInUser: null, - providerName: null, - login() {}, - logout() {}, -}); diff --git a/lib/browser-main.tsx b/lib/browser-main.tsx index 8f25d77..115e794 100644 --- a/lib/browser-main.tsx +++ b/lib/browser-main.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState } from "react"; import ReactDOM from "react-dom"; -import { FirebaseGithubAuthProvider } from "./auth"; +import { FirebaseAppProvider, FirebaseGithubAuthProvider } from "./firebase"; import { PageContext, PAGE_QUERY_ARG } from "./page"; import { pageNames, Pages, toPageName, DEFAULT_PAGE } from "./pages"; @@ -57,11 +57,13 @@ const App: React.FC<{}> = (props) => { }; return ( - - - - - + + + + + + + ); }; diff --git a/lib/firebase.tsx b/lib/firebase.tsx new file mode 100644 index 0000000..29af6fe --- /dev/null +++ b/lib/firebase.tsx @@ -0,0 +1,92 @@ +import { FirebaseApp, FirebaseOptions, initializeApp } from "firebase/app"; +import { + getAuth, + signInWithPopup, + GithubAuthProvider, + onAuthStateChanged, + signOut, + Auth, + User, +} from "firebase/auth"; +import React, { useCallback, useContext, useEffect, useState } from "react"; +import { AuthContext } from "./auth-context"; + +const DEFAULT_APP_CONFIG: FirebaseOptions = { + apiKey: "AIzaSyAV1kkVvSKEicEa8rLke9o_BxYBu1rb8kw", + authDomain: "mystic-addaf.firebaseapp.com", + projectId: "mystic-addaf", + storageBucket: "mystic-addaf.appspot.com", + messagingSenderId: "26787182745", + appId: "1:26787182745:web:e4fbd9439b9279fe966008", + measurementId: "G-JHKRSK1PR6", +}; + +type FirebaseAppContext = { + app: FirebaseApp; + auth: Auth; + provider: GithubAuthProvider; +}; + +export const FirebaseAppContext = + React.createContext(null); + +/** + * A Firebase app provider. Any other components that use Firebase must + * be a child of this. + * + * Note this component is assumed to never be unmounted, nor + * for its non-children props to change. + */ +export const FirebaseAppProvider: React.FC<{ config?: FirebaseOptions }> = ({ + config, + children, +}) => { + const [value, setValue] = useState(null); + + useEffect(() => { + const app = initializeApp(config || DEFAULT_APP_CONFIG); + const auth = getAuth(app); + const provider = new GithubAuthProvider(); + + setValue({ app, auth, provider }); + }, [config]); + + return ; +}; + +/** + * A Firebase GitHub authentication provider. Must be a child of a + * `FirebaseAppProvider`. + * + * Note this component is assumed to never be unmounted. + */ +export const FirebaseGithubAuthProvider: React.FC<{}> = ({ children }) => { + const appCtx = useContext(FirebaseAppContext); + const [user, setUser] = useState(null); + const [error, setError] = useState(undefined); + + const handleError = (e: Error) => setError(e.message); + + useEffect(() => { + if (!appCtx) return; + + onAuthStateChanged(appCtx.auth, setUser); + }, [appCtx]); + + const context: AuthContext = { + loggedInUser: user && user.displayName, + providerName: appCtx && "GitHub", + error, + login: useCallback(() => { + setError(undefined); + appCtx && + signInWithPopup(appCtx.auth, appCtx.provider).catch(handleError); + }, [appCtx]), + logout: useCallback(() => { + setError(undefined); + appCtx && signOut(appCtx.auth).catch(handleError); + }, [appCtx]), + }; + + return ; +}; diff --git a/lib/pages/debug-page.tsx b/lib/pages/debug-page.tsx index c9e797a..20f1f92 100644 --- a/lib/pages/debug-page.tsx +++ b/lib/pages/debug-page.tsx @@ -1,5 +1,5 @@ import React, { useContext, useState } from "react"; -import { AuthContext } from "../auth"; +import { AuthContext } from "../auth-context"; import { AutoSizingSvg } from "../auto-sizing-svg"; import { CreatureContext, CreatureContextType } from "../creature-symbol"; import { createCreatureSymbolFactory } from "../creature-symbol-factory";