@rebasepro/firebase 0.0.1-canary.0

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