@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.
- package/LICENSE +21 -0
- package/README.md +4 -0
- package/dist/components/FirebaseLoginView.d.ts +72 -0
- package/dist/components/RebaseFirebaseApp.d.ts +19 -0
- package/dist/components/RebaseFirebaseAppProps.d.ts +144 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/social_icons.d.ts +6 -0
- package/dist/hooks/index.d.ts +8 -0
- package/dist/hooks/useAppCheck.d.ts +20 -0
- package/dist/hooks/useBuildUserManagement.d.ts +46 -0
- package/dist/hooks/useFirebaseAuthController.d.ts +15 -0
- package/dist/hooks/useFirebaseRealTimeDBDelegate.d.ts +5 -0
- package/dist/hooks/useFirebaseStorageSource.d.ts +14 -0
- package/dist/hooks/useFirestoreDriver.d.ts +56 -0
- package/dist/hooks/useInitialiseFirebase.d.ts +34 -0
- package/dist/hooks/useRecaptcha.d.ts +8 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.es.js +3060 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +3043 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/social_icons.d.ts +6 -0
- package/dist/types/appcheck.d.ts +10 -0
- package/dist/types/auth.d.ts +41 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/text_search.d.ts +39 -0
- package/dist/utils/algolia.d.ts +9 -0
- package/dist/utils/collections_firestore.d.ts +5 -0
- package/dist/utils/database.d.ts +2 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/local_text_search_controller.d.ts +2 -0
- package/dist/utils/pinecone.d.ts +24 -0
- package/dist/utils/rebase_search_controller.d.ts +73 -0
- package/dist/utils/text_search_controller.d.ts +13 -0
- package/package.json +63 -0
- package/src/components/FirebaseLoginView.tsx +693 -0
- package/src/components/RebaseFirebaseApp.tsx +291 -0
- package/src/components/RebaseFirebaseAppProps.tsx +180 -0
- package/src/components/index.ts +3 -0
- package/src/components/social_icons.tsx +135 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/useAppCheck.ts +101 -0
- package/src/hooks/useBuildUserManagement.tsx +374 -0
- package/src/hooks/useFirebaseAuthController.ts +334 -0
- package/src/hooks/useFirebaseRealTimeDBDelegate.ts +269 -0
- package/src/hooks/useFirebaseStorageSource.ts +207 -0
- package/src/hooks/useFirestoreDriver.ts +784 -0
- package/src/hooks/useInitialiseFirebase.ts +132 -0
- package/src/hooks/useRecaptcha.tsx +28 -0
- package/src/index.ts +4 -0
- package/src/social_icons.tsx +135 -0
- package/src/types/appcheck.ts +11 -0
- package/src/types/auth.tsx +74 -0
- package/src/types/index.ts +3 -0
- package/src/types/text_search.ts +42 -0
- package/src/utils/algolia.ts +27 -0
- package/src/utils/collections_firestore.ts +148 -0
- package/src/utils/database.ts +39 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/local_text_search_controller.ts +143 -0
- package/src/utils/pinecone.ts +75 -0
- package/src/utils/rebase_search_controller.ts +357 -0
- 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
|
+
}
|