Replaced FirebaseUi with custom implementation

This commit is contained in:
francesco
2021-11-25 16:19:14 +01:00
parent 77388a78d5
commit d03414e9e6
19 changed files with 780 additions and 104 deletions

View File

@@ -8,7 +8,7 @@ import "typeface-rubik";
import "typeface-space-mono";
import {
AuthDelegate,
FirebaseAuthDelegate,
Authenticator,
buildCollection,
buildSchema,
@@ -75,6 +75,8 @@ const productSchema = buildSchema({
*/
export function CustomCMSApp() {
const signInOptions = DEFAULT_SIGN_IN_OPTIONS;
const navigation: NavigationBuilder = ({ user }: NavigationBuilderProps) => ({
collections: [
buildCollection({
@@ -102,8 +104,9 @@ export function CustomCMSApp() {
firebaseConfigError
} = useInitialiseFirebase({ firebaseConfig });
const authDelegate: AuthDelegate = useFirebaseAuthDelegate({
const authDelegate: FirebaseAuthDelegate = useFirebaseAuthDelegate({
firebaseApp,
signInOptions
});
const dataSource = useFirestoreDataSource({
@@ -149,7 +152,7 @@ export function CustomCMSApp() {
component = (
<FirebaseLoginView
skipLoginButtonEnabled={false}
signInOptions={DEFAULT_SIGN_IN_OPTIONS}
signInOptions={signInOptions}
firebaseApp={firebaseApp}
authDelegate={authDelegate}/>
);

View File

@@ -1,11 +1,7 @@
import React from "react";
import { getAnalytics } from "firebase/analytics";
import {
EmailAuthProvider,
GoogleAuthProvider,
User as FirebaseUser
} from "firebase/auth";
import { User as FirebaseUser } from "firebase/auth";
import {
Authenticator,
buildCollection,
@@ -154,11 +150,17 @@ function SampleApp() {
user,
authController
}) => {
if(user?.email?.includes("flanders")){
throw Error("Stupid Flanders!");
}
// This is an example of retrieving async data related to the user
// and storing it in the user extra field
const sampleUserData = await Promise.resolve({
roles: ["admin"]
});
authController.setExtra(sampleUserData);
console.log("Allowing access to", user);
return true;
@@ -191,8 +193,14 @@ function SampleApp() {
name={"My Online Shop"}
authentication={myAuthenticator}
signInOptions={[
GoogleAuthProvider.PROVIDER_ID,
EmailAuthProvider.PROVIDER_ID
'password',
// 'anonymous',
'google.com',
// 'facebook.com',
'github.com',
// 'twitter.com',
// 'microsoft.com',
// 'apple.com'
]}
textSearchController={textSearchController}
allowSkipLogin={true}

View File

@@ -232,6 +232,11 @@ export default function App() {
user,
authController
}) => {
if(user?.email?.includes("flanders")){
throw Error("Stupid Flanders!");
}
console.log("Allowing access to", user?.email);
// This is an example of retrieving async data related to the user
// and storing it in the user extra field.

View File

@@ -38,7 +38,6 @@
"@uiw/react-md-editor": "^3.4.10",
"date-fns": "^2.21.3",
"deep-equal": "^2.0.5",
"firebaseui": "~0.600",
"formik": "^2.2.9",
"history": "^5",
"object-hash": "^2.2.0",

View File

@@ -185,7 +185,7 @@ export function FireCMS<UserType>(props: FireCMSProps<UserType>) {
const schemaRegistryController = useBuildSchemaRegistryController(navigationContext, schemaResolver);
const sideEntityController = useBuildSideEntityController(navigationContext, schemaRegistryController);
const loading = authDelegate.authLoading || navigationContext.loading;
const loading = authController.authLoading || authController.initialLoading || navigationContext.loading;
if (navigationContext.navigationLoadingError) {
return (

View File

@@ -16,7 +16,7 @@ export const useStyles = makeStyles((theme: Theme) =>
margin: theme.spacing(1)
},
text: {
paddingLeft: theme.spacing(1)
paddingLeft: theme.spacing(2)
}
})
);

View File

@@ -1,7 +1,7 @@
import React, {lazy, Suspense, useCallback, useEffect, useState} from "react";
import {
Box,
CircularProgress,
CircularProgress, Divider,
IconButton,
Tab,
Tabs,
@@ -436,6 +436,8 @@ export function EntityView<M extends { [Key: string]: any }, UserType>({
}}/>
</Tabs>
<Divider/>
<Box flexGrow={1}/>
{dataLoading &&

View File

@@ -73,6 +73,10 @@ export function FireCMSAppBar({
const authController = useAuthController();
const { mode, toggleMode } = useModeState();
const initial = authController.user?.displayName ?
authController.user.displayName[0].toUpperCase()
: (authController.user?.email ? authController.user.email[0].toUpperCase() : "A");
return (
<Slide
direction="down" in={true} mountOnEnter unmountOnExit>
@@ -157,7 +161,7 @@ export function FireCMSAppBar({
<Avatar
src={authController.user.photoURL}/>
:
<Avatar>{authController.user?.displayName ? authController.user.displayName[0] : "A"}</Avatar>
<Avatar>{initial}</Avatar>
}
</Box>

View File

@@ -26,12 +26,14 @@ export function useBuildAuthController<UserType>({
}): AuthController<UserType> {
const [user, setUser] = useState<User | null>(null);
const [authLoading, setAuthLoading] = useState<boolean>(false);
const [notAllowedError, setNotAllowedError] = useState<any>(false);
const [extra, setExtra] = useState<any>();
async function checkAuthentication() {
const delegateUser = authDelegate.user;
if (authentication instanceof Function && delegateUser) {
setAuthLoading(true);
try {
const allowed = await authentication({
user: delegateUser,
@@ -47,7 +49,9 @@ export function useBuildAuthController<UserType>({
setNotAllowedError(true);
} catch (e) {
setNotAllowedError(e);
authDelegate.signOut();
}
setAuthLoading(false);
} else {
setUser(delegateUser);
}
@@ -66,6 +70,8 @@ export function useBuildAuthController<UserType>({
user,
loginSkipped,
canAccessMainView,
initialLoading: authDelegate.initialLoading ?? false,
authLoading: authLoading ,
notAllowedError,
signOut: authDelegate.signOut,
extra,

View File

@@ -19,7 +19,7 @@ import { useFirestoreDataSource } from "./hooks/useFirestoreDataSource";
import { useFirebaseStorageSource } from "./hooks/useFirebaseStorageSource";
import { useInitialiseFirebase } from "./hooks/useInitialiseFirebase";
import { FirebaseLoginView } from "./components/FirebaseLoginView";
import { AuthDelegate } from "../models";
import { FirebaseAuthDelegate } from "./models/auth";
const DEFAULT_SIGN_IN_OPTIONS = [
GoogleAuthProvider.PROVIDER_ID
@@ -51,7 +51,7 @@ export function FirebaseCMSApp({
navigation,
textSearchController,
allowSkipLogin,
signInOptions,
signInOptions = DEFAULT_SIGN_IN_OPTIONS,
firebaseConfig,
onFirebaseInit,
primaryColor,
@@ -71,8 +71,9 @@ export function FirebaseCMSApp({
firebaseConfigError
} = useInitialiseFirebase({ onFirebaseInit, firebaseConfig });
const authDelegate: AuthDelegate = useFirebaseAuthDelegate({
firebaseApp
const authDelegate: FirebaseAuthDelegate = useFirebaseAuthDelegate({
firebaseApp,
signInOptions
});
const dataSource = useFirestoreDataSource({

View File

@@ -9,6 +9,7 @@ import {
} from "../models";
import { FirestoreTextSearchController } from "./models/text_search";
import { User as FirebaseUser } from "firebase/auth";
import { FirebaseSignInOption, FirebaseSignInProvider } from "./models/auth";
/**
* Main entry point that defines the CMS configuration
@@ -52,7 +53,7 @@ export interface FirebaseCMSAppProps {
* objects such as specified in https://firebase.google.com/docs/auth/web/firebaseui
* Defaults to Google sign in only.
*/
signInOptions?: Array<string | any>;
signInOptions?: Array<FirebaseSignInProvider | FirebaseSignInOption>;
/**
* If authentication is enabled, allow the user to access the content

View File

@@ -1,24 +1,45 @@
import React, { useEffect } from "react";
import { Box, Button, Grid, Theme } from "@mui/material";
import React, { useEffect, useRef, useState } from "react";
import {
Box,
Button,
CircularProgress,
Grid,
IconButton,
TextField,
Theme,
Typography
} from "@mui/material";
import createStyles from "@mui/styles/createStyles";
import makeStyles from "@mui/styles/makeStyles";
import firebase from "firebase/compat/app";
import * as firebaseui from "firebaseui";
import "firebaseui/dist/firebaseui.css";
import { FirebaseApp } from "firebase/app";
import { FireCMSLogo } from "../../core/components/FireCMSLogo";
import { AuthDelegate } from "../../models";
import { useAuthController } from "../../hooks";
import { useAuthController, useModeState } from "../../hooks";
import {
FirebaseAuthDelegate,
FirebaseSignInOption,
FirebaseSignInProvider
} from "../models/auth";
import {
appleIcon,
facebookIcon,
githubIcon,
googleIcon,
microsoftIcon,
twitterIcon
} from "./social_icons";
import { ErrorView } from "../../core";
import EmailIcon from '@mui/icons-material/Email';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import PersonOutlineIcon from '@mui/icons-material/PersonOutline';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
logo: {
padding: theme.spacing(3),
maxWidth: 260
width: 260,
height: 260
}
})
);
@@ -29,10 +50,9 @@ const useStyles = makeStyles((theme: Theme) =>
export interface FirebaseLoginViewProps {
skipLoginButtonEnabled?: boolean,
logo?: string,
// Any of the sign in string or configuration objects defined in https://firebase.google.com/docs/auth/web/firebaseui
signInOptions: Array<string | any>;
signInOptions: Array<FirebaseSignInProvider | FirebaseSignInOption>;
firebaseApp: FirebaseApp;
authDelegate: AuthDelegate
authDelegate: FirebaseAuthDelegate
}
/**
@@ -50,61 +70,45 @@ export function FirebaseLoginView({
}: FirebaseLoginViewProps) {
const classes = useStyles();
const authController = useAuthController();
const modeState = useModeState();
useEffect(() => {
if (firebase.apps.length === 0) {
try {
firebase.initializeApp(firebaseApp.options);
} catch (e) {
console.error(e);
}
}
const [passwordLoginSelected, setPasswordLoginSelected] = useState(false);
const ui = firebaseui.auth.AuthUI.getInstance() || new firebaseui.auth.AuthUI(firebase.auth());
const uiConfig: firebaseui.auth.Config = {
callbacks: {
signInSuccessWithAuthResult: (authResult) => {
return true;
},
signInFailure: async (e) => {
console.error("signInFailure", e);
}
},
signInFlow: "popup",
signInOptions: signInOptions,
credentialHelper: firebaseui.auth.CredentialHelper.GOOGLE_YOLO
};
ui.start("#firebase-ui", uiConfig);
});
const resolvedSignInOptions: FirebaseSignInProvider[] = signInOptions.map((o) => {
if (typeof o === "object") {
return o.provider;
} else return o as FirebaseSignInProvider;
})
function buildErrorView() {
let errorView: any;
const ignoredCodes = ["auth/popup-closed-by-user", "auth/cancelled-popup-request"];
if (authDelegate.authError) {
if (authDelegate.authError.code === "auth/operation-not-allowed") {
errorView =
<>
<Box p={2}>
You need to enable the corresponding login provider
in your Firebase project
<Box p={1}>
<ErrorView
error={"You need to enable the corresponding login provider in your Firebase project"}/>
</Box>
{firebaseApp &&
<Box p={2}>
<Box p={1}>
<a href={`https://console.firebase.google.com/project/${firebaseApp.options.projectId}/authentication/providers`}
rel="noopener noreferrer"
target="_blank">
<Button variant="outlined"
<Button variant="text"
color="primary">
Open Firebase configuration
</Button>
</a>
</Box>}
</>;
} else {
} else if (!ignoredCodes.includes(authDelegate.authError.code)) {
console.error(authDelegate.authError);
errorView =
<Box p={2}>
{authDelegate.authError.message}
<Box p={1}>
<ErrorView error={authDelegate.authError.message}/>
</Box>;
}
}
@@ -126,45 +130,309 @@ export function FirebaseLoginView({
if (authController.notAllowedError) {
if (typeof authController.notAllowedError === "string") {
notAllowedMessage = authController.notAllowedError;
} else if (authController.notAllowedError instanceof Error) {
notAllowedMessage = authController.notAllowedError.message;
} else {
notAllowedMessage = "It looks like you don't have access to the CMS, based on the specified Authenticator configuration";
}
}
return (
<Grid
container
spacing={1}
direction="column"
alignItems="center"
justifyContent="center"
style={{ minHeight: "100vh" }}
>
<Box sx={{
display: 'flex',
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
p: 2
}}>
<Box sx={{
display: 'flex',
flexDirection: "column",
alignItems: "center",
width: "100%",
maxWidth: 340
}}>
<Box m={1}>
{logoComponent}
</Box>
<div id="firebase-ui"/>
{skipLoginButtonEnabled &&
<Box m={2}>
<Button onClick={authDelegate.skipLogin}>
Skip login
</Button>
</Box>
}
<Grid item xs={12}>
<Box m={1}>
{logoComponent}
</Box>
{notAllowedMessage &&
<Box p={2}>
{notAllowedMessage}
<ErrorView
error={notAllowedMessage}/>
</Box>}
{buildErrorView()}
</Grid>
</Grid>
{!passwordLoginSelected && <>
{buildOauthLoginButtons(authDelegate, resolvedSignInOptions, modeState.mode)}
{resolvedSignInOptions.includes("password") && <LoginButton
text={"Email/password"}
icon={<EmailIcon fontSize={"large"}/>}
onClick={() => setPasswordLoginSelected(true)}/>}
{resolvedSignInOptions.includes("anonymous") && <LoginButton
text={"Log in anonymously"}
icon={<PersonOutlineIcon fontSize={"large"}/>}
onClick={authDelegate.anonymousLogin}/>}
{skipLoginButtonEnabled &&
<Box m={1}>
<Button onClick={authDelegate.skipLogin}>
Skip login
</Button>
</Box>
}
</>}
{passwordLoginSelected && <LoginForm
authDelegate={authDelegate}
onClose={() => setPasswordLoginSelected(false)}
mode={modeState.mode}/>}
</Box>
</Box>
);
}
function LoginButton({
icon,
onClick,
text
}: { icon: React.ReactNode, onClick: () => void, text: string }) {
return (
<Box m={0.5} width={"100%"}>
<Button fullWidth
variant="outlined"
onClick={onClick}>
<Box sx={{
p: '1',
display: 'flex',
width: "240px",
height: "32px",
alignItems: "center",
justifyItems: "center"
}}>
<Box sx={{
display: 'flex',
flexDirection: "column",
width: "32px",
alignItems: "center",
justifyItems: "center"
}}>
{icon}
</Box>
<Box sx={{
flexGrow: 1,
pl: 2,
textAlign: "center"
}}>{text}</Box>
</Box>
</Button>
</Box>
)
}
function LoginForm({
onClose,
authDelegate,
mode
}: { onClose: () => void, authDelegate: FirebaseAuthDelegate, mode: "light" | "dark" }) {
const passwordRef = useRef<HTMLInputElement | null>(null);
const [email, setEmail] = useState<string>();
const [availableProviders, setAvailableProviders] = useState<string[] | undefined>();
const [password, setPassword] = useState<string>();
const shouldShowEmail = availableProviders === undefined;
const loginMode = availableProviders && availableProviders.includes('password');
const otherProvidersMode = availableProviders && !availableProviders.includes('password') && availableProviders.length > 0;
const registrationMode = availableProviders && !availableProviders.includes('password');
useEffect(() => {
console.log("loginMode", loginMode);
if ((loginMode || registrationMode) && passwordRef.current) {
passwordRef.current.focus()
}
}, [loginMode]);
useEffect(() => {
if (!document) return;
const escFunction = (event: any) => {
if (event.keyCode === 27) {
onClose();
}
};
document.addEventListener("keydown", escFunction, false);
return () => {
document.removeEventListener("keydown", escFunction, false);
};
}, [document]);
function handleEnterEmail() {
if (email) {
authDelegate.fetchSignInMethodsForEmail(email).then(setAvailableProviders);
}
}
function handleEnterPassword() {
if (email && password) {
authDelegate.emailPasswordLogin(email, password);
}
}
function handleRegistration() {
if (email && password) {
authDelegate.createUserWithEmailAndPassword(email, password);
}
}
const onBackPressed = () => {
if (shouldShowEmail) {
onClose();
} else {
setAvailableProviders(undefined);
}
}
const handleSubmit = (event: any) => {
event.preventDefault();
if (shouldShowEmail) {
handleEnterEmail();
} else if (loginMode) {
handleEnterPassword();
} else if (registrationMode) {
handleRegistration();
}
}
const label = registrationMode ? "No user found with that email. Pick a password to create a new account"
: (loginMode ? "Please enter your password" : "Please enter your email");
const button = registrationMode ? "Create account" : (loginMode ? "Login" : "Ok");
if (otherProvidersMode) {
return (
<Grid container spacing={1}>
<Grid item xs={12}>
<IconButton
onClick={onBackPressed}>
<ArrowBackIcon sx={{ width: 20, height: 20 }}/>
</IconButton>
</Grid>
<Grid item xs={12} sx={{ p: 1 }}>
<Typography align={"center"} variant={"subtitle2"}>
You already have an account
</Typography>
<Typography align={"center"} variant={"body2"}>
You can use one of these
methods to login with {email}
</Typography>
</Grid>
<Grid item xs={12}>
{buildOauthLoginButtons(authDelegate, availableProviders, mode)}
</Grid>
</Grid>
);
}
return (
<form onSubmit={handleSubmit}>
<Grid container spacing={1}>
<Grid item xs={12}>
<IconButton
onClick={onBackPressed}>
<ArrowBackIcon sx={{ width: 20, height: 20 }}/>
</IconButton>
</Grid>
<Grid item xs={12} sx={{ p: 1 }}>
<Typography align={"center"}
variant={"subtitle2"}>{label}</Typography>
</Grid>
<Grid item xs={12}
sx={{ display: shouldShowEmail ? "inherit" : "none" }}>
<TextField placeholder="Email" fullWidth autoFocus
value={email}
disabled={authDelegate.authLoading}
type="email"
onChange={(event) => setEmail(event.target.value)}/>
</Grid>
<Grid item xs={12}
sx={{ display: loginMode || registrationMode ? "inherit" : "none" }}>
<TextField placeholder="Password" fullWidth
value={password}
disabled={authDelegate.authLoading}
inputRef={passwordRef}
type="password"
onChange={(event) => setPassword(event.target.value)}/>
</Grid>
<Grid item xs={12}>
<Box sx={{
display: "flex",
justifyContent: "end",
alignItems: "center",
width: "100%"
}}>
{authDelegate.authLoading &&
<CircularProgress sx={{ p: 1 }} size={16}
thickness={8}/>
}
<Button type="submit">
{button}
</Button>
</Box>
</Grid>
</Grid>
</form>
);
}
function buildOauthLoginButtons(authDelegate: FirebaseAuthDelegate, providers: string[], mode: "light" | "dark") {
return <>
{providers.includes("google.com") && <LoginButton
text={"Sign in with Google"}
icon={googleIcon(mode)}
onClick={authDelegate.googleLogin}/>}
{providers.includes("microsoft.com") && <LoginButton
text={"Sign in with Microsoft"}
icon={microsoftIcon(mode)}
onClick={authDelegate.microsoftLogin}/>}
{providers.includes("apple.com") && <LoginButton
text={"Sign in with Apple"}
icon={appleIcon(mode)}
onClick={authDelegate.appleLogin}/>}
{providers.includes("github.com") && <LoginButton
text={"Sign in with Github"}
icon={githubIcon(mode)}
onClick={authDelegate.githubLogin}/>}
{providers.includes("facebook.com") && <LoginButton
text={"Sign in with Facebook"}
icon={facebookIcon(mode)}
onClick={authDelegate.facebookLogin}/>}
{providers.includes("twitter.com") && <LoginButton
text={"Sign in with Twitter"}
icon={twitterIcon(mode)}
onClick={authDelegate.twitterLogin}/>}
</>
}

View File

@@ -0,0 +1,138 @@
export const googleIcon = (mode: "light" | "dark") => <>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 64 64"
width={32}
height={32}
>
<linearGradient
id="95yY7w43Oj6n2vH63j6HJb"
x1="29.401"
x2="29.401"
y1="4.064"
y2="106.734"
gradientTransform="matrix(1 0 0 -1 0 66)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#ff5840"/>
<stop offset=".007" stopColor="#ff5840"/>
<stop offset=".989" stopColor="#fa528c"/>
<stop offset="1" stopColor="#fa528c"/>
</linearGradient>
<path
fill="url(#95yY7w43Oj6n2vH63j6HJb)"
d="M47.46,15.5l-1.37,1.48c-1.34,1.44-3.5,1.67-5.15,0.6c-2.71-1.75-6.43-3.13-11-2.37 c-4.94,0.83-9.17,3.85-11.64,
7.97l-8.03-6.08C14.99,9.82,23.2,5,32.5,5c5,0,9.94,1.56,14.27,4.46 C48.81,10.83,49.13,13.71,47.46,15.5z"
/>
<linearGradient
id="95yY7w43Oj6n2vH63j6HJc"
x1="12.148"
x2="12.148"
y1=".872"
y2="47.812"
gradientTransform="matrix(1 0 0 -1 0 66)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#feaa53"/>
<stop offset=".612" stopColor="#ffcd49"/>
<stop offset="1" stopColor="#ffde44"/>
</linearGradient>
<path
fill="url(#95yY7w43Oj6n2vH63j6HJc)"
d="M16.01,30.91c-0.09,2.47,0.37,4.83,1.27,6.96l-8.21,6.05c-1.35-2.51-2.3-5.28-2.75-8.22 c-1.06-6.88,0.54-13.38,
3.95-18.6l8.03,6.08C16.93,25.47,16.1,28.11,16.01,30.91z"
/>
<linearGradient
id="95yY7w43Oj6n2vH63j6HJd"
x1="29.76"
x2="29.76"
y1="32.149"
y2="-6.939"
gradientTransform="matrix(1 0 0 -1 0 66)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#42d778"/>
<stop offset=".428" stopColor="#3dca76"/>
<stop offset="1" stopColor="#34b171"/>
</linearGradient>
<path
fill="url(#95yY7w43Oj6n2vH63j6HJd)"
d="M50.45,51.28c-4.55,4.07-10.61,6.57-17.36,6.71C22.91,58.2,13.66,52.53,9.07,43.92l8.21-6.05 C19.78,43.81,
25.67,48,32.5,48c3.94,0,7.52-1.28,10.33-3.44L50.45,51.28z"
/>
<linearGradient
id="95yY7w43Oj6n2vH63j6HJe"
x1="46"
x2="46"
y1="3.638"
y2="35.593"
gradientTransform="matrix(1 0 0 -1 0 66)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#155cde"/>
<stop offset=".278" stopColor="#1f7fe5"/>
<stop offset=".569" stopColor="#279ceb"/>
<stop offset=".82" stopColor="#2cafef"/>
<stop offset="1" stopColor="#2eb5f0"/>
</linearGradient>
<path
fill="url(#95yY7w43Oj6n2vH63j6HJe)"
d="M59,31.97c0.01,7.73-3.26,14.58-8.55,19.31l-7.62-6.72c2.1-1.61,3.77-3.71,4.84-6.15
c0.29-0.66-0.2-1.41-0.92-1.41H37c-2.21,0-4-1.79-4-4v-2c0-2.21,1.79-4,4-4h17C56.75,27,59,29.22,59,31.97z"
/>
</svg>
</>;
export const appleIcon = (mode: "light" | "dark") => <svg width={32} height={32}
viewBox="0 0 56 56"
style={{ transform: 'scale(2.8)' }}
version="1.1"
xmlns="http://www.w3.org/2000/svg">
<g stroke={mode === "light" ? "#424245" : "white"} strokeWidth="0.5"
fillRule="evenodd">
<path
d="M28.2226562,20.3846154 C29.0546875,20.3846154 30.0976562,19.8048315 30.71875,19.0317864 C31.28125,18.3312142 31.6914062,17.352829 31.6914062,16.3744437 C31.6914062,16.2415766 31.6796875,16.1087095 31.65625,16 C30.7304687,16.0362365 29.6171875,16.640178 28.9492187,17.4494596 C28.421875,18.06548 27.9414062,19.0317864 27.9414062,20.0222505 C27.9414062,20.1671964 27.9648438,20.3121424 27.9765625,20.3604577 C28.0351562,20.3725366 28.1289062,20.3846154 28.2226562,20.3846154 Z M25.2929688,35 C26.4296875,35 26.9335938,34.214876 28.3515625,34.214876 C29.7929688,34.214876 30.109375,34.9758423 31.375,34.9758423 C32.6171875,34.9758423 33.4492188,33.792117 34.234375,32.6325493 C35.1132812,31.3038779 35.4765625,29.9993643 35.5,29.9389701 C35.4179688,29.9148125 33.0390625,28.9122695 33.0390625,26.0979021 C33.0390625,23.6579784 34.9140625,22.5588048 35.0195312,22.474253 C33.7773438,20.6382708 31.890625,20.5899555 31.375,20.5899555 C29.9804688,20.5899555 28.84375,21.4596313 28.1289062,21.4596313 C27.3554688,21.4596313 26.3359375,20.6382708 25.1289062,20.6382708 C22.8320312,20.6382708 20.5,22.5950413 20.5,26.2911634 C20.5,28.5861411 21.3671875,31.013986 22.4335938,32.5842339 C23.3476562,33.9129053 24.1445312,35 25.2929688,35 Z"
fill={mode === "light" ? "#424245" : "white"} fillRule="nonzero"/>
</g>
</svg>;
export const githubIcon = (mode: "light" | "dark") => <svg
fill={mode === "light" ? "#1c1e21" : "white"}
role="img"
viewBox="0 0 24 24"
width={28}
height={28}
xmlns="http://www.w3.org/2000/svg">
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>;
export const facebookIcon = (mode: "light" | "dark") => <svg
xmlns="http://www.w3.org/2000/svg"
width={28} height={28}
viewBox="0 0 90 90">
<g>
<path
d="M90,15.001C90,7.119,82.884,0,75,0H15C7.116,0,0,7.119,0,15.001v59.998 C0,82.881,7.116,90,15.001,90H45V56H34V41h11v-5.844C45,25.077,52.568,16,61.875,16H74v15H61.875C60.548,31,59,32.611,59,35.024V41 h15v15H59v34h16c7.884,0,15-7.119,15-15.001V15.001z"
fill={mode === "light" ? "#39569c" : "white"}/>
</g>
</svg>;
export const microsoftIcon = (mode: "light" | "dark") => <svg
xmlns="http://www.w3.org/2000/svg" width={28} height={28}
viewBox="0 0 480 480">
<g>
<path
d="M0.176,224L0.001,67.963l192-26.072V224H0.176z M224.001,37.241L479.937,0v224H224.001V37.241z M479.999,256l-0.062,224 l-255.936-36.008V256H479.999z M192.001,439.918L0.157,413.621L0.147,256h191.854V439.918z"
fill={mode === "light" ? "#00a2ed" : "white"}/>
</g>
</svg>;
export const twitterIcon = (mode: "light" | "dark") => <svg
xmlns="http://www.w3.org/2000/svg" width={28} height={28}
viewBox="0 0 24 24">
<path fill={mode === "light" ? "#00acee" : "white"}
d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/>
</svg>;

View File

@@ -1,16 +1,36 @@
import { useEffect, useState } from "react";
import {
Auth,
FacebookAuthProvider,
fetchSignInMethodsForEmail,
getAuth,
GoogleAuthProvider,
OAuthProvider,
onAuthStateChanged,
signInWithEmailAndPassword,
signInWithPopup,
signOut,
User as FirebaseUser
} from "firebase/auth";
import { FirebaseApp } from "firebase/app";
import { AuthDelegate } from "../../models";
import {
FirebaseAuthDelegate,
FirebaseSignInOption,
FirebaseSignInProvider
} from "../models/auth";
import firebase from "firebase/compat";
import {
createUserWithEmailAndPassword,
signInAnonymously
} from "@firebase/auth";
import GithubAuthProvider = firebase.auth.GithubAuthProvider;
import TwitterAuthProvider = firebase.auth.TwitterAuthProvider;
interface FirebaseAuthHandlerProps {
firebaseApp?: FirebaseApp,
firebaseApp?: FirebaseApp;
signInOptions: Array<FirebaseSignInProvider | FirebaseSignInOption>;
}
/**
@@ -19,11 +39,13 @@ interface FirebaseAuthHandlerProps {
*/
export const useFirebaseAuthDelegate = (
{
firebaseApp
}: FirebaseAuthHandlerProps): AuthDelegate<FirebaseUser> => {
firebaseApp,
signInOptions
}: FirebaseAuthHandlerProps): FirebaseAuthDelegate => {
const [loggedUser, setLoggedUser] = useState<FirebaseUser | null>(null);
const [loggedUser, setLoggedUser] = useState<FirebaseUser | null | undefined>(undefined); // logged user, anonymous or logged out
const [authProviderError, setAuthProviderError] = useState<any>();
const [initialLoading, setInitialLoading] = useState(true);
const [authLoading, setAuthLoading] = useState(true);
const [loginSkipped, setLoginSkipped] = useState<boolean>(false);
@@ -35,6 +57,7 @@ export const useFirebaseAuthDelegate = (
useEffect(() => {
if (!firebaseApp) return;
const auth = getAuth(firebaseApp);
setLoggedUser(auth.currentUser)
return onAuthStateChanged(
auth,
updateFirebaseUser,
@@ -44,6 +67,7 @@ export const useFirebaseAuthDelegate = (
const updateFirebaseUser = async (firebaseUser: FirebaseUser | null) => {
setLoggedUser(firebaseUser);
setInitialLoading(false);
setAuthLoading(false);
};
@@ -54,14 +78,140 @@ export const useFirebaseAuthDelegate = (
setLoggedUser(null);
setAuthProviderError(null);
});
setLoginSkipped(false);
}
const getProviderOptions = (providerId: FirebaseSignInProvider): FirebaseSignInOption | undefined => {
return signInOptions.find((option) => {
if (option === null) throw Error("useFirebaseAuthDelegate");
if (typeof option === "object" && option.provider === providerId)
return option as FirebaseSignInOption;
return undefined;
}) as FirebaseSignInOption | undefined;
}
const googleLogin = () => {
const provider = new GoogleAuthProvider();
const options = getProviderOptions("google.com");
if (options?.scopes)
options.scopes.forEach((scope) => provider.addScope(scope));
if (options?.customParameters)
provider.setCustomParameters(options.customParameters);
const auth = getAuth();
signInWithPopup(auth, provider).catch(setAuthProviderError);
}
function doOauthLogin(auth: Auth, provider: OAuthProvider | FacebookAuthProvider | GithubAuthProvider | TwitterAuthProvider) {
setAuthLoading(true);
signInWithPopup(auth, provider)
.catch(setAuthProviderError)
.then(() => setAuthLoading(false));
}
const anonymousLogin = () => {
const auth = getAuth();
setAuthLoading(true);
signInAnonymously(auth)
.catch(setAuthProviderError)
.then(() => setAuthLoading(false));
}
const appleLogin = () => {
const provider = new OAuthProvider('apple.com');
const options = getProviderOptions('apple.com');
if (options?.scopes)
options.scopes.forEach((scope) => provider.addScope(scope));
if (options?.customParameters)
provider.setCustomParameters(options.customParameters);
const auth = getAuth();
doOauthLogin(auth, provider);
}
const facebookLogin = () => {
const provider = new FacebookAuthProvider();
const options = getProviderOptions('facebook.com');
if (options?.scopes)
options.scopes.forEach((scope) => provider.addScope(scope));
if (options?.customParameters)
provider.setCustomParameters(options.customParameters);
const auth = getAuth();
doOauthLogin(auth, provider);
}
const githubLogin = () => {
const provider = new GithubAuthProvider();
const options = getProviderOptions('github.com');
if (options?.scopes)
options.scopes.forEach((scope) => provider.addScope(scope));
if (options?.customParameters)
provider.setCustomParameters(options.customParameters);
const auth = getAuth();
doOauthLogin(auth, provider);
}
const microsoftLogin = () => {
const provider = new OAuthProvider('microsoft.com');
const options = getProviderOptions('microsoft.com');
if (options?.scopes)
options.scopes.forEach((scope) => provider.addScope(scope));
if (options?.customParameters)
provider.setCustomParameters(options.customParameters);
const auth = getAuth();
doOauthLogin(auth, provider);
}
const twitterLogin = () => {
const provider = new TwitterAuthProvider();
const options = getProviderOptions('twitter.com');
if (options?.customParameters)
provider.setCustomParameters(options.customParameters);
const auth = getAuth();
doOauthLogin(auth, provider);
}
const emailPasswordLogin = (email: string, password: string) => {
const auth = getAuth();
setAuthLoading(true);
signInWithEmailAndPassword(auth, email, password)
.catch(setAuthProviderError)
.then(() => setAuthLoading(false));
}
const registerWithPasswordEmail = (email: string, password: string) => {
const auth = getAuth();
setAuthLoading(true);
createUserWithEmailAndPassword(auth, email, password)
.catch(setAuthProviderError)
.then(() => setAuthLoading(false));
}
const getSignInMethodsForEmail = (email: string): Promise<string[]> => {
const auth = getAuth();
setAuthLoading(true);
return fetchSignInMethodsForEmail(auth, email)
.then((res) => {
setAuthLoading(false);
return res;
});
}
return {
user: loggedUser,
user: loggedUser ?? null,
authError: authProviderError,
authLoading,
initialLoading,
signOut: onSignOut,
loginSkipped,
skipLogin
skipLogin,
googleLogin,
anonymousLogin,
appleLogin,
facebookLogin,
githubLogin,
microsoftLogin,
twitterLogin,
emailPasswordLogin,
fetchSignInMethodsForEmail: getSignInMethodsForEmail,
createUserWithEmailAndPassword: registerWithPasswordEmail
};
};

View File

@@ -25,5 +25,7 @@ export {
export type { InitialiseFirebaseResult } from "./hooks/useInitialiseFirebase";
export { useInitialiseFirebase } from "./hooks/useInitialiseFirebase";
export type { FirebaseAuthDelegate } from "./models/auth";
export type { FirestoreTextSearchController } from "./models/text_search";
export { performAlgoliaTextSearch } from "./models/text_search";

View File

@@ -0,0 +1,69 @@
import { User as FirebaseUser } from "firebase/auth";
import { AuthDelegate } from "../../models";
/**
* @category Firebase
*/
export type FirebaseSignInProvider =
// | 'email'
| 'password'
// | 'phone'
| 'anonymous'
| 'google.com'
| 'facebook.com'
| 'github.com'
| 'twitter.com'
| 'microsoft.com'
| 'apple.com';
/**
* @category Firebase
*/
export type FirebaseSignInOption = {
provider: FirebaseSignInProvider;
scopes?: string[];
customParameters?: Record<string, string>;
}
/**
* @category Firebase
*/
export type FirebaseAuthDelegate =
AuthDelegate<FirebaseUser> & {
authLoading: boolean;
googleLogin: () => void;
anonymousLogin: () => void;
appleLogin: () => void;
facebookLogin: () => void;
githubLogin: () => void;
microsoftLogin: () => void;
twitterLogin: () => void;
emailPasswordLogin: (email: string, password: string) => void;
fetchSignInMethodsForEmail: (email: string) => Promise<string[]>;
createUserWithEmailAndPassword: (email: string, password: string) => void;
/**
* Has the user skipped the login process
*/
loginSkipped?: boolean;
/**
* Skip login
*/
skipLogin?: () => void;
};

View File

@@ -25,6 +25,19 @@ export interface AuthController<UserType extends User = User> {
*/
canAccessMainView: boolean;
/**
* Initial loading flag. It is used not to display the login screen
* when the app first loads and it has not been checked whether the user
* is logged in or not.
*/
initialLoading: boolean;
/**
* If you have defined an {@link Authenticator}, this flag will be set to
* true while it loads
*/
authLoading: boolean;
/**
* The current user was not allowed access
*/
@@ -79,9 +92,11 @@ export type AuthDelegate<UserType extends User = User> = {
authError?: any;
/**
* Is the login process ongoing
* Initial loading flag. It is used not to display the login screen
* when the app first loads and it has not been checked whether the user
* is logged in or not.
*/
authLoading: boolean;
initialLoading?: boolean;
/**
* Sign out

View File

@@ -287,6 +287,11 @@ export default function App() {
user,
authController
}) => {
// You can throw an error to display a message
if(user?.email?.includes("flanders")){
throw Error("Stupid Flanders!");
}
console.log("Allowing access to", user?.email);
// This is an example of retrieving async data related to the user
// and storing it in the user extra field.

View File

@@ -6875,10 +6875,10 @@ firebase@^9.4.1:
"@firebase/storage-compat" "0.1.8"
"@firebase/util" "1.4.2"
firebaseui@~0.600:
version "0.600.0"
resolved "https://registry.yarnpkg.com/firebaseui/-/firebaseui-0.600.0.tgz#4d9b1ab978e86d23f306952fa347ffdb7ebac510"
integrity sha512-5iaRcuaaXTmCIU+VNJHIerzz1bgcfGmPTZCFL/yLHsQ6o3KeUggr/mY2rmMh2aMT38UgpyV5QndwEAZDiOeOWg==
firebaseui@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/firebaseui/-/firebaseui-6.0.0.tgz#074580e9e7da93ed3d551a869dbab4e0902701d7"
integrity sha512-fhzteivWRTnOhSk5JkIVLmcXk6aVh4ZtW+KAnJijeeZhAyM+Ed9m1YgC4l+VK+NOp+kerVw6HL1mRdvDF6Yeuw==
dependencies:
dialog-polyfill "^0.4.7"
material-design-lite "^1.2.0"