@rebasepro/client-firebase 0.0.1-canary.09e5ec5

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