@rebasepro/client-firebase 0.0.1-canary.4d4fb3e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -0
  3. package/dist/components/FirebaseLoginView.d.ts +72 -0
  4. package/dist/components/RebaseFirebaseApp.d.ts +19 -0
  5. package/dist/components/RebaseFirebaseAppProps.d.ts +144 -0
  6. package/dist/components/index.d.ts +3 -0
  7. package/dist/components/social_icons.d.ts +6 -0
  8. package/dist/hooks/index.d.ts +7 -0
  9. package/dist/hooks/useAppCheck.d.ts +20 -0
  10. package/dist/hooks/useFirebaseAuthController.d.ts +15 -0
  11. package/dist/hooks/useFirebaseRealTimeDBDelegate.d.ts +5 -0
  12. package/dist/hooks/useFirebaseStorageSource.d.ts +14 -0
  13. package/dist/hooks/useFirestoreDriver.d.ts +56 -0
  14. package/dist/hooks/useInitialiseFirebase.d.ts +34 -0
  15. package/dist/hooks/useRecaptcha.d.ts +8 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.es.js +2757 -0
  18. package/dist/index.es.js.map +1 -0
  19. package/dist/index.umd.js +2743 -0
  20. package/dist/index.umd.js.map +1 -0
  21. package/dist/social_icons.d.ts +6 -0
  22. package/dist/types/appcheck.d.ts +10 -0
  23. package/dist/types/auth.d.ts +41 -0
  24. package/dist/types/index.d.ts +3 -0
  25. package/dist/types/text_search.d.ts +39 -0
  26. package/dist/utils/algolia.d.ts +9 -0
  27. package/dist/utils/collections_firestore.d.ts +5 -0
  28. package/dist/utils/database.d.ts +2 -0
  29. package/dist/utils/index.d.ts +7 -0
  30. package/dist/utils/local_text_search_controller.d.ts +2 -0
  31. package/dist/utils/pinecone.d.ts +24 -0
  32. package/dist/utils/rebase_search_controller.d.ts +73 -0
  33. package/dist/utils/text_search_controller.d.ts +13 -0
  34. package/package.json +61 -0
  35. package/src/components/FirebaseLoginView.tsx +703 -0
  36. package/src/components/RebaseFirebaseApp.tsx +275 -0
  37. package/src/components/RebaseFirebaseAppProps.tsx +180 -0
  38. package/src/components/index.ts +3 -0
  39. package/src/components/social_icons.tsx +135 -0
  40. package/src/hooks/index.ts +7 -0
  41. package/src/hooks/useAppCheck.ts +101 -0
  42. package/src/hooks/useFirebaseAuthController.ts +334 -0
  43. package/src/hooks/useFirebaseRealTimeDBDelegate.ts +269 -0
  44. package/src/hooks/useFirebaseStorageSource.ts +208 -0
  45. package/src/hooks/useFirestoreDriver.ts +778 -0
  46. package/src/hooks/useInitialiseFirebase.ts +132 -0
  47. package/src/hooks/useRecaptcha.tsx +28 -0
  48. package/src/index.ts +4 -0
  49. package/src/social_icons.tsx +135 -0
  50. package/src/types/appcheck.ts +11 -0
  51. package/src/types/auth.tsx +74 -0
  52. package/src/types/index.ts +3 -0
  53. package/src/types/text_search.ts +42 -0
  54. package/src/utils/algolia.ts +27 -0
  55. package/src/utils/collections_firestore.ts +149 -0
  56. package/src/utils/database.ts +39 -0
  57. package/src/utils/index.ts +7 -0
  58. package/src/utils/local_text_search_controller.ts +143 -0
  59. package/src/utils/pinecone.ts +75 -0
  60. package/src/utils/rebase_search_controller.ts +356 -0
  61. package/src/utils/text_search_controller.ts +34 -0
@@ -0,0 +1,703 @@
1
+ import React, { ReactNode, useCallback, useEffect, useRef, useState } from "react";
2
+
3
+ import { FirebaseApp, FirebaseError } from "@firebase/app";
4
+ import { ErrorView, RebaseLogo, useModeController, useSnackbarController, } from "@rebasepro/core";
5
+ import {
6
+ ArrowBackIcon,
7
+ Button,
8
+ CallIcon,
9
+ CircularProgress,
10
+ cls,
11
+ IconButton,
12
+ LoadingButton,
13
+ MailIcon,
14
+ PersonIcon,
15
+ TextField,
16
+ Typography,
17
+ } from "@rebasepro/ui";
18
+ import { appleIcon, facebookIcon, githubIcon, googleIcon, microsoftIcon, twitterIcon } from "./social_icons";
19
+ import {
20
+ getAuth,
21
+ getMultiFactorResolver,
22
+ MultiFactorError,
23
+ PhoneAuthProvider,
24
+ PhoneMultiFactorGenerator,
25
+ RecaptchaVerifier
26
+ } from "@firebase/auth";
27
+ import {
28
+ FirebaseAuthController,
29
+ FirebaseSignInOption,
30
+ FirebaseSignInProvider,
31
+ RECAPTCHA_CONTAINER_ID,
32
+ useRecaptcha
33
+ } from "../index";
34
+
35
+ /**
36
+ * @category Firebase
37
+ */
38
+ export interface FirebaseLoginViewProps {
39
+
40
+ /**
41
+ * Firebase app this login view is accessing
42
+ */
43
+ firebaseApp: FirebaseApp;
44
+
45
+ /**
46
+ * Delegate holding the auth state
47
+ */
48
+ authController: FirebaseAuthController;
49
+
50
+ /**
51
+ * Path to the logo displayed in the login screen
52
+ */
53
+ logo?: string;
54
+
55
+ /**
56
+ * Enable the skip login button
57
+ */
58
+ allowSkipLogin?: boolean;
59
+
60
+ /**
61
+ * Each of the sign in options that get a custom button
62
+ */
63
+ signInOptions: Array<FirebaseSignInProvider | FirebaseSignInOption>;
64
+
65
+ /**
66
+ * Disable the login buttons
67
+ */
68
+ disabled?: boolean;
69
+
70
+ /**
71
+ * Prevent users from creating new users in when the `signInOptions` value
72
+ * is `password`. This does not apply to the rest of login providers.
73
+ */
74
+ disableSignupScreen?: boolean;
75
+
76
+ /**
77
+ * Prevent users from resetting their password when the `signInOptions` value
78
+ * is `password`. This does not apply to the rest of login providers.
79
+ */
80
+ disableResetPassword?: boolean;
81
+
82
+ /**
83
+ * Display this component when no user is found a user tries to log in
84
+ * when the `signInOptions` value is `password`.
85
+ */
86
+ noUserComponent?: ReactNode;
87
+
88
+ /**
89
+ * Include additional components in the login view, on top of the login buttons.
90
+ */
91
+ children?: ReactNode;
92
+
93
+ /**
94
+ * Display this component bellow the sign-in buttons.
95
+ * Useful for adding checkboxes for privacy and terms and conditions.
96
+ * You may want to use it in conjunction with the `disabled` prop.
97
+ */
98
+ additionalComponent?: ReactNode;
99
+
100
+ notAllowedError?: any;
101
+
102
+ className?: string;
103
+
104
+ }
105
+
106
+ /**
107
+ * Use this component to render a login view, that updates
108
+ * the state of the {@link FirebaseAuthController} based on the result
109
+
110
+ * @category Firebase
111
+ */
112
+ export function FirebaseLoginView({
113
+ children,
114
+ allowSkipLogin,
115
+ logo,
116
+ signInOptions,
117
+ firebaseApp,
118
+ authController,
119
+ noUserComponent,
120
+ disableSignupScreen = false,
121
+ disableResetPassword = false,
122
+ disabled = false,
123
+ additionalComponent,
124
+ notAllowedError,
125
+ className
126
+ }: FirebaseLoginViewProps) {
127
+
128
+ const modeState = useModeController();
129
+
130
+ const [passwordLoginSelected, setPasswordLoginSelected] = useState(false);
131
+
132
+ const [phoneLoginSelected, setPhoneLoginSelected] = useState(false);
133
+
134
+ const [fadeIn, setFadeIn] = useState(false);
135
+
136
+ useEffect(() => {
137
+ // Trigger the fade-in effect on component mount
138
+ const timer = setTimeout(() => {
139
+ setFadeIn(true);
140
+ }, 50); // Small delay to ensure transition works properly
141
+
142
+ return () => clearTimeout(timer);
143
+ }, []);
144
+
145
+ const resolvedSignInOptions: FirebaseSignInProvider[] = signInOptions.map((o) => {
146
+ if (typeof o === "object") {
147
+ return o.provider;
148
+ } else return o as FirebaseSignInProvider;
149
+ })
150
+
151
+ const sendMFASms = useCallback(() => {
152
+ const auth = getAuth(firebaseApp);
153
+ const recaptchaVerifier = new RecaptchaVerifier(auth, "recaptcha", { size: "invisible" });
154
+
155
+ const resolver = getMultiFactorResolver(auth, authController.authProviderError as MultiFactorError);
156
+
157
+ if (resolver.hints[0].factorId === PhoneMultiFactorGenerator.FACTOR_ID) {
158
+
159
+ const phoneInfoOptions = {
160
+ multiFactorHint: resolver.hints[0],
161
+ session: resolver.session
162
+ };
163
+ const phoneAuthProvider = new PhoneAuthProvider(auth);
164
+ // Send SMS verification code
165
+ phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
166
+ .then(function (verificationId) {
167
+
168
+ // Ask user for the SMS verification code. Then:
169
+ const verificationCode = String(window.prompt("Please enter the verification " + "code that was sent to your mobile device."));
170
+ const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
171
+ const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
172
+ // // Complete sign-in.
173
+ return resolver.resolveSignIn(multiFactorAssertion);
174
+
175
+ })
176
+
177
+ } else {
178
+ // Unsupported second factor.
179
+ console.warn("Unsupported second factor.");
180
+ }
181
+
182
+ }, [authController.authProviderError]);
183
+
184
+ function buildErrorView() {
185
+ let errorView: any;
186
+ if (authController.user != null) return errorView; // if the user is logged in via MFA
187
+ const ignoredCodes = ["auth/popup-closed-by-user", "auth/cancelled-popup-request"];
188
+ if (authController.authProviderError) {
189
+ const authError = authController.authProviderError as FirebaseError;
190
+ if (authError.code === "auth/operation-not-allowed" ||
191
+ authError.code === "auth/configuration-not-found") {
192
+ errorView =
193
+ <>
194
+ <div className="p-4">
195
+ <ErrorView
196
+ title={"Firebase Auth not enabled"}
197
+ error={"You need to enable Firebase Auth and the corresponding login provider in your Firebase project"} />
198
+ </div>
199
+ {firebaseApp &&
200
+ <div className="p-4">
201
+ <a href={`https://console.firebase.google.com/project/${firebaseApp.options.projectId}/authentication/providers`}
202
+ rel="noopener noreferrer"
203
+ target="_blank">
204
+ <Button variant="text"
205
+ color="error">
206
+ Open Firebase configuration
207
+ </Button>
208
+ </a>
209
+ </div>}
210
+ </>;
211
+ } else if (authError.code === "auth/invalid-api-key") {
212
+ errorView = <div className="p-4">
213
+ <ErrorView
214
+ title={"Invalid API key"}
215
+ error={"auth/invalid-api-key: Check that your Firebase config is set correctly in your `firebase_config.ts` file"} />
216
+ </div>;
217
+ } else if (authError.code === "auth/email-already-in-use") {
218
+ errorView = <div className="p-4">
219
+ <ErrorView
220
+ title={"Email already in use"}
221
+ error={"The selected email is already in use by another account"} />
222
+ </div>;
223
+ } else if (authError.code === "auth/invalid-credential") {
224
+ errorView = <div className="p-4">
225
+ <ErrorView
226
+ title={"Invalid credential"}
227
+ error={"The provided credential is not correct"} />
228
+ </div>;
229
+ } else if (!ignoredCodes.includes(authError.code)) {
230
+ if (authError.code === "auth/multi-factor-auth-required") {
231
+ sendMFASms();
232
+ }
233
+ errorView =
234
+ <div className="p-4">
235
+ <ErrorView error={authController.authProviderError as Error} />
236
+ </div>;
237
+ }
238
+ }
239
+ return errorView;
240
+ }
241
+
242
+ let logoComponent;
243
+ if (logo) {
244
+ logoComponent = <img src={logo}
245
+ style={{
246
+ height: "100%",
247
+ width: "100%",
248
+ objectFit: "contain"
249
+ }}
250
+ alt={"Logo"} />;
251
+ } else {
252
+ logoComponent = <RebaseLogo />;
253
+ }
254
+
255
+ let notAllowedMessage: string | undefined;
256
+ if (notAllowedError) {
257
+ if (typeof notAllowedError === "string") {
258
+ notAllowedMessage = notAllowedError;
259
+ } else if (notAllowedError instanceof Error) {
260
+ notAllowedMessage = notAllowedError.message;
261
+ } else {
262
+ notAllowedMessage = "It looks like you don't have access to the CMS, based on the specified Authenticator configuration";
263
+ }
264
+ }
265
+
266
+ const fadeStyle = {
267
+ opacity: fadeIn ? 1 : 0,
268
+ transition: "opacity 0.6s ease-in-out"
269
+ };
270
+
271
+ return (
272
+
273
+ <div
274
+ className={cls("flex flex-col items-center justify-center min-w-full p-4", className)}
275
+ style={fadeStyle}>
276
+ <div id="recaptcha"></div>
277
+ <div
278
+ className="flex flex-col items-center w-full max-w-[500px]">
279
+
280
+ <div className="p-1 w-64 h-64 m-4">
281
+ {logoComponent}
282
+ </div>
283
+
284
+ {children}
285
+
286
+ {notAllowedMessage &&
287
+ <div className="p-8">
288
+ <ErrorView error={notAllowedMessage} />
289
+ </div>}
290
+
291
+ {buildErrorView()}
292
+
293
+ {(!passwordLoginSelected && !phoneLoginSelected) && <div className={"my-4 w-full"}>
294
+
295
+ {buildOauthLoginButtons(authController, resolvedSignInOptions, modeState.mode, disabled)}
296
+
297
+ {resolvedSignInOptions.includes("password") &&
298
+ <LoginButton
299
+ disabled={disabled}
300
+ text={"Email/password"}
301
+ icon={<MailIcon size={28} />}
302
+ onClick={() => setPasswordLoginSelected(true)} />}
303
+
304
+ {resolvedSignInOptions.includes("phone") &&
305
+ <LoginButton
306
+ disabled={disabled}
307
+ text={"Phone number"}
308
+ icon={<CallIcon size={28} />}
309
+ onClick={() => setPhoneLoginSelected(true)} />}
310
+
311
+ {resolvedSignInOptions.includes("anonymous") &&
312
+ <LoginButton
313
+ disabled={disabled}
314
+ text={"Log in anonymously"}
315
+ icon={<PersonIcon
316
+ size={28} />}
317
+ onClick={authController.anonymousLogin} />}
318
+
319
+ {allowSkipLogin &&
320
+ <Button
321
+ className={"m-1 mb-4"}
322
+ variant={"text"}
323
+ disabled={disabled}
324
+ onClick={authController.skipLogin}>
325
+ Skip login
326
+ </Button>
327
+ }
328
+
329
+ </div>}
330
+
331
+ {passwordLoginSelected && <LoginForm
332
+ authController={authController}
333
+ onClose={() => setPasswordLoginSelected(false)}
334
+ mode={modeState.mode}
335
+ noUserComponent={noUserComponent}
336
+ disableSignupScreen={disableSignupScreen}
337
+ disableResetPassword={disableResetPassword}
338
+ />}
339
+
340
+ {phoneLoginSelected && <PhoneLoginForm
341
+ authController={authController}
342
+ onClose={() => setPhoneLoginSelected(false)}
343
+ />}
344
+
345
+ {!passwordLoginSelected && !phoneLoginSelected && additionalComponent}
346
+
347
+ </div>
348
+ </div>
349
+ );
350
+ }
351
+
352
+ export function LoginButton({
353
+ icon,
354
+ onClick,
355
+ text,
356
+ disabled
357
+ }: {
358
+ icon: React.ReactNode,
359
+ onClick: () => void,
360
+ text: string,
361
+ disabled?: boolean
362
+ }) {
363
+ return (
364
+ <div className="my-1 w-full">
365
+ <Button
366
+ className={cls("w-full bg-white dark:bg-surface-800 text-surface-900 dark:text-surface-100", disabled ? "" : "hover:text-surface-800 hover:dark:text-white")}
367
+ style={{
368
+ height: "40px",
369
+ borderRadius: "4px",
370
+ fontSize: "14px"
371
+ }}
372
+
373
+ disabled={disabled}
374
+ onClick={onClick}>
375
+ <div
376
+ className="p-1 flex h-8 items-center justify-items-center">
377
+ <div
378
+ className="flex flex-col w-8 items-center justify-items-center mr-4">
379
+ {icon}
380
+ </div>
381
+ <div className="grow pl-2 text-center">{text}</div>
382
+ </div>
383
+ </Button>
384
+ </div>
385
+ )
386
+ }
387
+
388
+ function PhoneLoginForm({
389
+ onClose,
390
+ authController
391
+ }: {
392
+ onClose: () => void,
393
+ authController: FirebaseAuthController,
394
+ }) {
395
+ useRecaptcha();
396
+
397
+ const [phone, setPhone] = useState<string>();
398
+ const [code, setCode] = useState<string>();
399
+ const [isInvalidCode, setIsInvalidCode] = useState(false);
400
+
401
+ const handleSubmit = async (event: any) => {
402
+ event.preventDefault();
403
+
404
+ if (code && authController.confirmationResult) {
405
+ setIsInvalidCode(false);
406
+
407
+ authController.confirmationResult.confirm(code).catch((e: FirebaseError) => {
408
+ if (e.code === "auth/invalid-verification-code") {
409
+ setIsInvalidCode(true)
410
+ }
411
+ });
412
+ } else {
413
+ if (phone) {
414
+ authController.phoneLogin(phone, window.recaptchaVerifier);
415
+ }
416
+ }
417
+ }
418
+
419
+ return (
420
+ <form onSubmit={handleSubmit}>
421
+ {isInvalidCode &&
422
+ <div className="p-8">
423
+ <ErrorView error={"Invalid confirmation code"} />
424
+ </div>}
425
+
426
+ <div id={RECAPTCHA_CONTAINER_ID} />
427
+
428
+ <div className={"flex flex-col gap-1"}>
429
+ <IconButton
430
+ onClick={onClose}>
431
+ <ArrowBackIcon className="w-5 h-5" />
432
+ </IconButton>
433
+ <div className="p-1 flex">
434
+ <Typography align={"center"}
435
+ variant={"subtitle2"}>{"Please enter your phone number"}</Typography>
436
+ </div>
437
+ <TextField placeholder=""
438
+ value={phone ?? ""}
439
+ disabled={Boolean(phone && (authController.authLoading || authController.confirmationResult))}
440
+ type="phone"
441
+ onChange={(event) => setPhone(event.target.value)} />
442
+ {Boolean(phone && authController.confirmationResult) &&
443
+ <>
444
+ <div className="mt-2 p-1 flex">
445
+ <Typography align={"center"}
446
+ variant={"subtitle2"}>{"Please enter the confirmation code"}</Typography>
447
+ </div>
448
+ <TextField placeholder=""
449
+ value={code ?? ""}
450
+ type="text"
451
+ onChange={(event) => setCode(event.target.value)} />
452
+ </>
453
+ }
454
+
455
+ <div className="flex justify-end items-center w-full">
456
+
457
+ {authController.authLoading &&
458
+ <CircularProgress className="p-1" size={"small"} />
459
+ }
460
+
461
+ <Button type="submit">
462
+ {"Ok"}
463
+ </Button>
464
+ </div>
465
+
466
+ </div>
467
+ </form>
468
+ );
469
+ }
470
+
471
+ type LoginFormMode = "email" | "password" | "registration";
472
+
473
+ function LoginForm({
474
+ onClose,
475
+ authController,
476
+ mode,
477
+ noUserComponent,
478
+ disableSignupScreen,
479
+ disableResetPassword
480
+ }: {
481
+ onClose: () => void,
482
+ authController: FirebaseAuthController,
483
+ mode: "light" | "dark",
484
+ noUserComponent?: ReactNode,
485
+ disableSignupScreen: boolean,
486
+ disableResetPassword?: boolean
487
+ }) {
488
+
489
+ const passwordRef = useRef<HTMLInputElement | null>(null);
490
+
491
+ const [loginState, setLoginState] = useState<LoginFormMode>("email"); // ["email", "password", "registration"]
492
+ const [email, setEmail] = useState<string>();
493
+ const [password, setPassword] = useState<string>();
494
+ const [previouslyUsedMethodsForUser, setPreviouslyUsedMethodsForUser] = useState<string[] | undefined>();
495
+ const [resettingPassword, setResettingPassword] = useState(false);
496
+
497
+ const snackbarController = useSnackbarController();
498
+
499
+ useEffect(() => {
500
+ if ((loginState === "password" || loginState === "registration") && passwordRef.current) {
501
+ passwordRef.current.focus()
502
+ }
503
+ }, [loginState]);
504
+
505
+ useEffect(() => {
506
+ if (!document) return;
507
+ const escFunction = (event: any) => {
508
+ if (event.keyCode === 27) {
509
+ onClose();
510
+ }
511
+ };
512
+ document.addEventListener("keydown", escFunction, false);
513
+ return () => {
514
+ document.removeEventListener("keydown", escFunction, false);
515
+ };
516
+ }, [onClose]);
517
+
518
+ function handleEnterEmail() {
519
+ if (email) {
520
+ authController.fetchSignInMethodsForEmail(email).then((availableProviders) => {
521
+ setPreviouslyUsedMethodsForUser(availableProviders.filter(p => p !== "password"));
522
+ });
523
+ setLoginState("password");
524
+ }
525
+ }
526
+
527
+ function handleEnterPassword() {
528
+ if (email && password) {
529
+ authController.emailPasswordLogin(email, password);
530
+ }
531
+ }
532
+
533
+ function handleRegistration() {
534
+ if (email && password) {
535
+ authController.createUserWithEmailAndPassword(email, password);
536
+ }
537
+ }
538
+
539
+ const onBackPressed = () => {
540
+ if (loginState === "email") {
541
+ onClose();
542
+ } else if (loginState === "password" || loginState === "registration") {
543
+ setLoginState("email");
544
+ } else {
545
+ setPreviouslyUsedMethodsForUser(undefined);
546
+ }
547
+ }
548
+
549
+ const handleSubmit = (event: any) => {
550
+ event.preventDefault();
551
+ if (loginState === "email") {
552
+ handleEnterEmail();
553
+ } else if (loginState === "password") {
554
+ handleEnterPassword();
555
+ } else if (loginState === "registration") {
556
+ handleRegistration();
557
+ }
558
+ }
559
+
560
+ const label = loginState === "registration"
561
+ ? "Please enter your email and password to create an account"
562
+ : (loginState === "password" ? "Please enter your password" : "Please enter your email");
563
+
564
+ return (
565
+ <form
566
+ className={"w-full"}
567
+ onSubmit={handleSubmit}>
568
+
569
+ <div className={"max-w-[480px] w-full flex flex-col gap-4"}>
570
+ <IconButton
571
+ onClick={onBackPressed}>
572
+ <ArrowBackIcon className="w-5 h-5" />
573
+ </IconButton>
574
+
575
+ <div>
576
+ {loginState === "registration" && noUserComponent}
577
+ </div>
578
+
579
+ <Typography
580
+ className={`${loginState === "registration" && disableSignupScreen ? "hidden" : "flex"}`}
581
+ variant={"subtitle2"}>{label}</Typography>
582
+
583
+ {(loginState === "email" || loginState === "registration") && <TextField placeholder="Email" autoFocus
584
+ value={email ?? ""}
585
+ disabled={authController.authLoading}
586
+ type="email"
587
+ onChange={(event) => setEmail(event.target.value)} />}
588
+
589
+ <div
590
+ className={`${loginState === "password" || (loginState === "registration" && !disableSignupScreen) ? "block" : "hidden"}`}>
591
+ <TextField placeholder="Password"
592
+ value={password ?? ""}
593
+ disabled={authController.authLoading}
594
+ inputRef={passwordRef}
595
+ type="password"
596
+ onChange={(event) => setPassword(event.target.value)} />
597
+ </div>
598
+
599
+ <div
600
+ className={`${loginState === "registration" && disableSignupScreen ? "hidden" : "flex"} justify-end items-center w-full flex gap-2`}>
601
+
602
+ {authController.authLoading &&
603
+ <CircularProgress className="p-1" size={"small"} />
604
+ }
605
+
606
+ {!disableResetPassword && <LoadingButton variant="text"
607
+ loading={resettingPassword}
608
+ onClick={email
609
+ ? async () => {
610
+ setResettingPassword(true);
611
+ try {
612
+ try {
613
+ await authController.sendPasswordResetEmail(email);
614
+ snackbarController.open({
615
+ message: "Password reset email sent",
616
+ type: "success"
617
+ });
618
+ } catch (e: any) {
619
+ snackbarController.open({
620
+ message: e.message,
621
+ type: "error"
622
+ });
623
+ }
624
+ } finally {
625
+ setResettingPassword(false);
626
+ }
627
+ }
628
+ : undefined}>
629
+ Reset password
630
+ </LoadingButton>}
631
+
632
+ {!disableSignupScreen && loginState === "email" &&
633
+ <Button variant="text" onClick={() => setLoginState("registration")}>
634
+ New user
635
+ </Button>}
636
+
637
+ <Button type="submit">
638
+ {loginState === "registration" ? "Create account" : (loginState === "password" ? "Login" : "Login")}
639
+ </Button>
640
+ </div>
641
+
642
+ {previouslyUsedMethodsForUser && previouslyUsedMethodsForUser.length > 0 &&
643
+ <div className={"flex flex-col gap-4 p-4"}>
644
+ <div>
645
+ <Typography variant={"subtitle2"}>
646
+ You already have an account
647
+ </Typography>
648
+ <Typography variant={"body2"}>
649
+ You can use one of these
650
+ methods to login with {email}
651
+ </Typography>
652
+ </div>
653
+
654
+ <div>
655
+ {previouslyUsedMethodsForUser && buildOauthLoginButtons(authController, previouslyUsedMethodsForUser, mode, false)}
656
+ </div>
657
+ </div>
658
+ }
659
+ </div>
660
+ </form>
661
+ );
662
+
663
+ }
664
+
665
+ function buildOauthLoginButtons(authController: FirebaseAuthController, providers: string[], mode: "light" | "dark", disabled: boolean) {
666
+ return <>
667
+ {providers.includes("google.com") && <LoginButton
668
+ disabled={disabled}
669
+ text={"Sign in with Google"}
670
+ icon={googleIcon(mode)}
671
+ onClick={authController.googleLogin} />}
672
+
673
+ {providers.includes("microsoft.com") && <LoginButton
674
+ disabled={disabled}
675
+ text={"Sign in with Microsoft"}
676
+ icon={microsoftIcon(mode)}
677
+ onClick={authController.microsoftLogin} />}
678
+
679
+ {providers.includes("apple.com") && <LoginButton
680
+ disabled={disabled}
681
+ text={"Sign in with Apple"}
682
+ icon={appleIcon(mode)}
683
+ onClick={authController.appleLogin} />}
684
+
685
+ {providers.includes("github.com") && <LoginButton
686
+ disabled={disabled}
687
+ text={"Sign in with Github"}
688
+ icon={githubIcon(mode)}
689
+ onClick={authController.githubLogin} />}
690
+
691
+ {providers.includes("facebook.com") && <LoginButton
692
+ disabled={disabled}
693
+ text={"Sign in with Facebook"}
694
+ icon={facebookIcon(mode)}
695
+ onClick={authController.facebookLogin} />}
696
+
697
+ {providers.includes("twitter.com") && <LoginButton
698
+ disabled={disabled}
699
+ text={"Sign in with Twitter"}
700
+ icon={twitterIcon(mode)}
701
+ onClick={authController.twitterLogin} />}
702
+ </>
703
+ }