@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.
- 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 +7 -0
- package/dist/hooks/useAppCheck.d.ts +20 -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 +2757 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +2743 -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 +61 -0
- package/src/components/FirebaseLoginView.tsx +703 -0
- package/src/components/RebaseFirebaseApp.tsx +275 -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 +7 -0
- package/src/hooks/useAppCheck.ts +101 -0
- package/src/hooks/useFirebaseAuthController.ts +334 -0
- package/src/hooks/useFirebaseRealTimeDBDelegate.ts +269 -0
- package/src/hooks/useFirebaseStorageSource.ts +208 -0
- package/src/hooks/useFirestoreDriver.ts +778 -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 +149 -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 +356 -0
- package/src/utils/text_search_controller.ts +34 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
import { AppCheck, getToken, initializeAppCheck } from "@firebase/app-check";
|
|
4
|
+
import { FirebaseApp } from "@firebase/app";
|
|
5
|
+
import { AppCheckOptions } from "../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @group Firebase
|
|
9
|
+
*/
|
|
10
|
+
export interface InitializeAppCheckProps {
|
|
11
|
+
firebaseApp?: FirebaseApp;
|
|
12
|
+
options?: AppCheckOptions;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface InitializeAppCheckResult {
|
|
16
|
+
loading: boolean;
|
|
17
|
+
appCheckVerified?: boolean;
|
|
18
|
+
error?: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Function used to initialise Firebase App Check.
|
|
23
|
+
*
|
|
24
|
+
* @group Firebase
|
|
25
|
+
*/
|
|
26
|
+
export function useAppCheck({
|
|
27
|
+
firebaseApp,
|
|
28
|
+
options,
|
|
29
|
+
}: InitializeAppCheckProps): InitializeAppCheckResult {
|
|
30
|
+
if (options?.debugToken) {
|
|
31
|
+
Object.assign(window, {
|
|
32
|
+
FIREBASE_APPCHECK_DEBUG_TOKEN: options?.debugToken
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const [appCheckLoading, setAppCheckLoading] = React.useState<boolean>(false);
|
|
37
|
+
const [appCheckVerified, setAppCheckVerified] = React.useState<boolean | undefined>(undefined);
|
|
38
|
+
const [error, setError] = React.useState<any>();
|
|
39
|
+
|
|
40
|
+
const initialCheck = useRef<boolean>(false);
|
|
41
|
+
|
|
42
|
+
const verifyToken = useCallback(async (appCheck: AppCheck) => {
|
|
43
|
+
console.debug("Verifying App Check token...", appCheck);
|
|
44
|
+
try {
|
|
45
|
+
const token = await getToken(appCheck, options?.forceRefresh);
|
|
46
|
+
console.debug("App Check token:", token);
|
|
47
|
+
if (!token) {
|
|
48
|
+
setError("App Check failed.");
|
|
49
|
+
setAppCheckVerified(false);
|
|
50
|
+
} else {
|
|
51
|
+
setAppCheckVerified(true);
|
|
52
|
+
console.debug("App Check success.");
|
|
53
|
+
}
|
|
54
|
+
} catch (e: any) {
|
|
55
|
+
console.error("App Check error:", e);
|
|
56
|
+
setError(e.message);
|
|
57
|
+
}
|
|
58
|
+
}, [options?.forceRefresh]);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!options) return;
|
|
62
|
+
if (!firebaseApp) return;
|
|
63
|
+
if (appCheckVerified !== undefined) return;
|
|
64
|
+
if (initialCheck.current) return;
|
|
65
|
+
|
|
66
|
+
setAppCheckLoading(true);
|
|
67
|
+
|
|
68
|
+
const {
|
|
69
|
+
provider,
|
|
70
|
+
isTokenAutoRefreshEnabled
|
|
71
|
+
} = options;
|
|
72
|
+
|
|
73
|
+
removeCurrentAppCheckDiv();
|
|
74
|
+
|
|
75
|
+
const appCheck = initializeAppCheck(firebaseApp, {
|
|
76
|
+
provider,
|
|
77
|
+
isTokenAutoRefreshEnabled
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
verifyToken(appCheck)
|
|
81
|
+
.then(() => {
|
|
82
|
+
setAppCheckLoading(false);
|
|
83
|
+
});
|
|
84
|
+
initialCheck.current = true;
|
|
85
|
+
}, [appCheckVerified, firebaseApp, options, verifyToken]);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
loading: appCheckLoading,
|
|
89
|
+
appCheckVerified,
|
|
90
|
+
error
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
function removeCurrentAppCheckDiv() {
|
|
96
|
+
const div = document.getElementById("fire_app_check_[DEFAULT]");
|
|
97
|
+
if (div) {
|
|
98
|
+
div.remove();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { deepEqual as equal } from "fast-equals"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ApplicationVerifier,
|
|
6
|
+
Auth,
|
|
7
|
+
ConfirmationResult,
|
|
8
|
+
createUserWithEmailAndPassword as createUserWithEmailAndPasswordFirebase,
|
|
9
|
+
FacebookAuthProvider,
|
|
10
|
+
fetchSignInMethodsForEmail as fetchSignInMethodsForEmailFirebase,
|
|
11
|
+
getAuth,
|
|
12
|
+
GithubAuthProvider,
|
|
13
|
+
GoogleAuthProvider,
|
|
14
|
+
OAuthProvider,
|
|
15
|
+
onAuthStateChanged,
|
|
16
|
+
sendPasswordResetEmail as sendPasswordResetEmailFirebase,
|
|
17
|
+
signInAnonymously,
|
|
18
|
+
signInWithEmailAndPassword,
|
|
19
|
+
signInWithPhoneNumber,
|
|
20
|
+
signInWithPopup,
|
|
21
|
+
signOut,
|
|
22
|
+
TwitterAuthProvider,
|
|
23
|
+
User as FirebaseUser
|
|
24
|
+
} from "@firebase/auth";
|
|
25
|
+
import { FirebaseApp } from "@firebase/app";
|
|
26
|
+
import { FirebaseAuthController, FirebaseSignInOption, FirebaseSignInProvider, FirebaseUserWrapper } from "../types";
|
|
27
|
+
import { Role, User } from "@rebasepro/types";
|
|
28
|
+
|
|
29
|
+
export interface FirebaseAuthControllerProps {
|
|
30
|
+
loading?: boolean;
|
|
31
|
+
firebaseApp?: FirebaseApp;
|
|
32
|
+
signInOptions?: Array<FirebaseSignInProvider | FirebaseSignInOption>;
|
|
33
|
+
onSignOut?: () => void;
|
|
34
|
+
defineRolesFor?: (user: User) => Promise<Role[] | undefined> | Role[] | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Use this hook to build an {@link AuthController} based on Firebase Auth
|
|
39
|
+
* @group Firebase
|
|
40
|
+
*/
|
|
41
|
+
export const useFirebaseAuthController = <USER extends FirebaseUserWrapper = any, ExtraData = any>({
|
|
42
|
+
loading,
|
|
43
|
+
firebaseApp,
|
|
44
|
+
signInOptions,
|
|
45
|
+
onSignOut: onSignOutProp,
|
|
46
|
+
defineRolesFor
|
|
47
|
+
}: FirebaseAuthControllerProps): FirebaseAuthController<USER, ExtraData> => {
|
|
48
|
+
|
|
49
|
+
const [loggedUser, setLoggedUser] = useState<FirebaseUser | null | undefined>(undefined); // logged user, anonymous or logged out
|
|
50
|
+
const [authError, setAuthError] = useState<any>();
|
|
51
|
+
const [authProviderError, setAuthProviderError] = useState<any>();
|
|
52
|
+
const [initialLoading, setInitialLoading] = useState<boolean>(true);
|
|
53
|
+
const [authLoading, setAuthLoading] = useState(true);
|
|
54
|
+
const [loginSkipped, setLoginSkipped] = useState<boolean>(false);
|
|
55
|
+
const [confirmationResult, setConfirmationResult] = useState<undefined | ConfirmationResult>();
|
|
56
|
+
const [userRoles, _setUserRoles] = useState<Role[] | undefined>();
|
|
57
|
+
const [extra, setExtra] = useState<any>();
|
|
58
|
+
|
|
59
|
+
const setUserRoles = useCallback((roles: Role[] | undefined) => {
|
|
60
|
+
const currentRoleIds = userRoles?.map(r => r.id);
|
|
61
|
+
const newRoleIds = roles?.map(r => r.id);
|
|
62
|
+
if (!equal(currentRoleIds, newRoleIds)) {
|
|
63
|
+
_setUserRoles(roles);
|
|
64
|
+
}
|
|
65
|
+
}, [userRoles]);
|
|
66
|
+
|
|
67
|
+
const authRef = useRef<Auth | null>(null);
|
|
68
|
+
|
|
69
|
+
const updateUser = useCallback(async (user: FirebaseUser | null, initialize?: boolean) => {
|
|
70
|
+
if (loading) return;
|
|
71
|
+
if (defineRolesFor && user) {
|
|
72
|
+
setUserRoles(await defineRolesFor(user));
|
|
73
|
+
}
|
|
74
|
+
setLoggedUser(user);
|
|
75
|
+
setAuthLoading(false);
|
|
76
|
+
if (initialize) {
|
|
77
|
+
setInitialLoading(false);
|
|
78
|
+
}
|
|
79
|
+
}, [loading]);
|
|
80
|
+
|
|
81
|
+
const updateRoles = useCallback(async (user: User | null) => {
|
|
82
|
+
if (defineRolesFor && user) {
|
|
83
|
+
const userRoles = await defineRolesFor(user);
|
|
84
|
+
if (!equal(userRoles, userRoles)) {
|
|
85
|
+
setUserRoles(userRoles);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}, [defineRolesFor, userRoles]);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (updateRoles && loggedUser) {
|
|
92
|
+
updateRoles(loggedUser);
|
|
93
|
+
}
|
|
94
|
+
}, [updateRoles, loggedUser]);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!firebaseApp) return;
|
|
98
|
+
try {
|
|
99
|
+
const auth = getAuth(firebaseApp);
|
|
100
|
+
authRef.current = auth;
|
|
101
|
+
setAuthError(undefined);
|
|
102
|
+
updateUser(auth.currentUser, false)
|
|
103
|
+
return onAuthStateChanged(
|
|
104
|
+
auth,
|
|
105
|
+
async (user) => {
|
|
106
|
+
console.debug("User state changed", user);
|
|
107
|
+
await updateUser(user, true);
|
|
108
|
+
},
|
|
109
|
+
error => setAuthProviderError(error)
|
|
110
|
+
);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
setAuthError(e);
|
|
113
|
+
setInitialLoading(false);
|
|
114
|
+
return () => {
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}, [firebaseApp, updateUser]);
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!loading && authRef.current) {
|
|
121
|
+
updateUser(authRef.current.currentUser, false);
|
|
122
|
+
}
|
|
123
|
+
}, [loading, updateUser]);
|
|
124
|
+
|
|
125
|
+
const getProviderOptions = useCallback((providerId: FirebaseSignInProvider): FirebaseSignInOption | undefined => {
|
|
126
|
+
return signInOptions?.find((option) => {
|
|
127
|
+
if (option === null) throw Error("useFirebaseAuthController");
|
|
128
|
+
if (typeof option === "object" && option.provider === providerId)
|
|
129
|
+
return option as FirebaseSignInOption;
|
|
130
|
+
return undefined;
|
|
131
|
+
}) as FirebaseSignInOption | undefined;
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
const googleLogin = useCallback(() => {
|
|
135
|
+
const provider = new GoogleAuthProvider();
|
|
136
|
+
const options = getProviderOptions("google.com");
|
|
137
|
+
if (options?.scopes)
|
|
138
|
+
options.scopes.forEach((scope) => provider.addScope(scope));
|
|
139
|
+
if (options?.customParameters) {
|
|
140
|
+
provider.setCustomParameters(options.customParameters);
|
|
141
|
+
} else {
|
|
142
|
+
provider.setCustomParameters({
|
|
143
|
+
prompt: "select_account"
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const auth = authRef.current;
|
|
147
|
+
if (!auth) throw Error("No auth");
|
|
148
|
+
signInWithPopup(auth, provider).catch(setAuthProviderError);
|
|
149
|
+
}, [getProviderOptions]);
|
|
150
|
+
|
|
151
|
+
const getAuthToken = useCallback(async (): Promise<string> => {
|
|
152
|
+
if (!loggedUser)
|
|
153
|
+
throw Error("No client user is logged in");
|
|
154
|
+
if (!loggedUser.getIdToken) {
|
|
155
|
+
throw Error("No getIdToken method available");
|
|
156
|
+
}
|
|
157
|
+
return loggedUser.getIdToken?.();
|
|
158
|
+
}, [loggedUser]);
|
|
159
|
+
|
|
160
|
+
const emailPasswordLogin = useCallback((email: string, password: string) => {
|
|
161
|
+
const auth = authRef.current;
|
|
162
|
+
if (!auth) throw Error("No auth");
|
|
163
|
+
setAuthLoading(true);
|
|
164
|
+
signInWithEmailAndPassword(auth, email, password)
|
|
165
|
+
.catch(setAuthProviderError)
|
|
166
|
+
.then(() => setAuthLoading(false));
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
const createUserWithEmailAndPassword = useCallback((email: string, password: string) => {
|
|
170
|
+
const auth = authRef.current;
|
|
171
|
+
if (!auth) throw Error("No auth");
|
|
172
|
+
setAuthLoading(true);
|
|
173
|
+
createUserWithEmailAndPasswordFirebase(auth, email, password)
|
|
174
|
+
.catch(setAuthProviderError)
|
|
175
|
+
.then(() => setAuthLoading(false));
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
const sendPasswordResetEmail = useCallback((email: string) => {
|
|
179
|
+
const auth = authRef.current;
|
|
180
|
+
if (!auth) throw Error("No auth");
|
|
181
|
+
return sendPasswordResetEmailFirebase(auth, email)
|
|
182
|
+
}, []);
|
|
183
|
+
|
|
184
|
+
const fetchSignInMethodsForEmail = useCallback((email: string): Promise<string[]> => {
|
|
185
|
+
const auth = authRef.current;
|
|
186
|
+
if (!auth) throw Error("No auth");
|
|
187
|
+
setAuthLoading(true);
|
|
188
|
+
return fetchSignInMethodsForEmailFirebase(auth, email)
|
|
189
|
+
.then((res) => {
|
|
190
|
+
setAuthLoading(false);
|
|
191
|
+
return res;
|
|
192
|
+
});
|
|
193
|
+
}, []);
|
|
194
|
+
|
|
195
|
+
const onSignOut = useCallback(async () => {
|
|
196
|
+
const auth = authRef.current;
|
|
197
|
+
if (!auth) throw Error("No auth");
|
|
198
|
+
await signOut(auth)
|
|
199
|
+
.then(_ => {
|
|
200
|
+
setLoggedUser(null);
|
|
201
|
+
setUserRoles(undefined);
|
|
202
|
+
setAuthProviderError(null);
|
|
203
|
+
onSignOutProp?.();
|
|
204
|
+
});
|
|
205
|
+
setLoginSkipped(false);
|
|
206
|
+
}, [onSignOutProp]);
|
|
207
|
+
|
|
208
|
+
const doOauthLogin = useCallback((auth: Auth, provider: OAuthProvider | FacebookAuthProvider | GithubAuthProvider | TwitterAuthProvider) => {
|
|
209
|
+
setAuthLoading(true);
|
|
210
|
+
signInWithPopup(auth, provider)
|
|
211
|
+
.catch(setAuthProviderError).then(() => setAuthLoading(false));
|
|
212
|
+
}, []);
|
|
213
|
+
|
|
214
|
+
const anonymousLogin = useCallback(() => {
|
|
215
|
+
const auth = authRef.current;
|
|
216
|
+
if (!auth) throw Error("No auth");
|
|
217
|
+
setAuthLoading(true);
|
|
218
|
+
signInAnonymously(auth)
|
|
219
|
+
.catch(setAuthProviderError)
|
|
220
|
+
.then(() => setAuthLoading(false));
|
|
221
|
+
}, []);
|
|
222
|
+
|
|
223
|
+
const phoneLogin = useCallback((phone: string, applicationVerifier: ApplicationVerifier) => {
|
|
224
|
+
const auth = authRef.current;
|
|
225
|
+
if (!auth) throw Error("No auth");
|
|
226
|
+
setAuthLoading(true);
|
|
227
|
+
return signInWithPhoneNumber(auth, phone, applicationVerifier)
|
|
228
|
+
.catch(setAuthProviderError)
|
|
229
|
+
.then((res) => {
|
|
230
|
+
setAuthLoading(false);
|
|
231
|
+
setConfirmationResult(res ?? undefined);
|
|
232
|
+
});
|
|
233
|
+
}, []);
|
|
234
|
+
|
|
235
|
+
const appleLogin = useCallback(() => {
|
|
236
|
+
const provider = new OAuthProvider("apple.com");
|
|
237
|
+
const options = getProviderOptions("apple.com");
|
|
238
|
+
if (options?.scopes)
|
|
239
|
+
options.scopes.forEach((scope) => provider.addScope(scope));
|
|
240
|
+
if (options?.customParameters)
|
|
241
|
+
provider.setCustomParameters(options.customParameters);
|
|
242
|
+
const auth = authRef.current;
|
|
243
|
+
if (!auth) throw Error("No auth");
|
|
244
|
+
doOauthLogin(auth, provider);
|
|
245
|
+
}, [doOauthLogin, getProviderOptions]);
|
|
246
|
+
|
|
247
|
+
const facebookLogin = useCallback(() => {
|
|
248
|
+
const provider = new FacebookAuthProvider();
|
|
249
|
+
const options = getProviderOptions("facebook.com");
|
|
250
|
+
if (options?.scopes)
|
|
251
|
+
options.scopes.forEach((scope) => provider.addScope(scope));
|
|
252
|
+
if (options?.customParameters)
|
|
253
|
+
provider.setCustomParameters(options.customParameters);
|
|
254
|
+
const auth = authRef.current;
|
|
255
|
+
if (!auth) throw Error("No auth");
|
|
256
|
+
doOauthLogin(auth, provider);
|
|
257
|
+
}, [doOauthLogin, getProviderOptions]);
|
|
258
|
+
|
|
259
|
+
const githubLogin = useCallback(() => {
|
|
260
|
+
const provider = new GithubAuthProvider();
|
|
261
|
+
const options = getProviderOptions("github.com");
|
|
262
|
+
if (options?.scopes)
|
|
263
|
+
options.scopes.forEach((scope) => provider.addScope(scope));
|
|
264
|
+
if (options?.customParameters)
|
|
265
|
+
provider.setCustomParameters(options.customParameters);
|
|
266
|
+
const auth = authRef.current;
|
|
267
|
+
if (!auth) throw Error("No auth");
|
|
268
|
+
doOauthLogin(auth, provider);
|
|
269
|
+
}, [doOauthLogin, getProviderOptions]);
|
|
270
|
+
|
|
271
|
+
const microsoftLogin = useCallback(() => {
|
|
272
|
+
const provider = new OAuthProvider("microsoft.com");
|
|
273
|
+
const options = getProviderOptions("microsoft.com");
|
|
274
|
+
if (options?.scopes)
|
|
275
|
+
options.scopes.forEach((scope) => provider.addScope(scope));
|
|
276
|
+
if (options?.customParameters)
|
|
277
|
+
provider.setCustomParameters(options.customParameters);
|
|
278
|
+
const auth = authRef.current;
|
|
279
|
+
if (!auth) throw Error("No auth");
|
|
280
|
+
doOauthLogin(auth, provider);
|
|
281
|
+
}, [doOauthLogin, getProviderOptions]);
|
|
282
|
+
|
|
283
|
+
const twitterLogin = useCallback(() => {
|
|
284
|
+
const provider = new TwitterAuthProvider();
|
|
285
|
+
const options = getProviderOptions("twitter.com");
|
|
286
|
+
if (options?.customParameters)
|
|
287
|
+
provider.setCustomParameters(options.customParameters);
|
|
288
|
+
const auth = authRef.current;
|
|
289
|
+
if (!auth) throw Error("No auth");
|
|
290
|
+
doOauthLogin(auth, provider);
|
|
291
|
+
}, [doOauthLogin, getProviderOptions]);
|
|
292
|
+
|
|
293
|
+
const skipLogin = useCallback(() => {
|
|
294
|
+
setLoginSkipped(true);
|
|
295
|
+
setLoggedUser(null);
|
|
296
|
+
setUserRoles(undefined);
|
|
297
|
+
}, []);
|
|
298
|
+
|
|
299
|
+
const firebaseUserWrapper: FirebaseUserWrapper | null = loggedUser
|
|
300
|
+
? {
|
|
301
|
+
...loggedUser,
|
|
302
|
+
roles: userRoles?.map(r => r.id), // User.roles is string[], keep Role[] internally only
|
|
303
|
+
firebaseUser: loggedUser
|
|
304
|
+
}
|
|
305
|
+
: null;
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
user: firebaseUserWrapper as USER,
|
|
309
|
+
setUser: updateUser,
|
|
310
|
+
setUserRoles,
|
|
311
|
+
authProviderError,
|
|
312
|
+
authLoading,
|
|
313
|
+
initialLoading: loading || initialLoading,
|
|
314
|
+
signOut: onSignOut,
|
|
315
|
+
getAuthToken,
|
|
316
|
+
googleLogin,
|
|
317
|
+
skipLogin,
|
|
318
|
+
loginSkipped,
|
|
319
|
+
emailPasswordLogin,
|
|
320
|
+
createUserWithEmailAndPassword,
|
|
321
|
+
sendPasswordResetEmail,
|
|
322
|
+
fetchSignInMethodsForEmail,
|
|
323
|
+
anonymousLogin,
|
|
324
|
+
phoneLogin,
|
|
325
|
+
appleLogin,
|
|
326
|
+
facebookLogin,
|
|
327
|
+
githubLogin,
|
|
328
|
+
microsoftLogin,
|
|
329
|
+
twitterLogin,
|
|
330
|
+
confirmationResult,
|
|
331
|
+
extra,
|
|
332
|
+
setExtra
|
|
333
|
+
};
|
|
334
|
+
};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { FirebaseApp } from "@firebase/app";
|
|
2
|
+
import {
|
|
3
|
+
Database,
|
|
4
|
+
get,
|
|
5
|
+
getDatabase,
|
|
6
|
+
limitToFirst,
|
|
7
|
+
onValue,
|
|
8
|
+
orderByChild,
|
|
9
|
+
orderByKey,
|
|
10
|
+
push,
|
|
11
|
+
query,
|
|
12
|
+
ref,
|
|
13
|
+
remove,
|
|
14
|
+
set,
|
|
15
|
+
startAt
|
|
16
|
+
} from "@firebase/database";
|
|
17
|
+
import { useCallback } from "react";
|
|
18
|
+
import { DataDriver, DeleteEntityProps, Entity, FetchCollectionProps, FetchEntityProps, ListenCollectionProps, ListenEntityProps, SaveEntityProps } from "@rebasepro/types";
|
|
19
|
+
|
|
20
|
+
export function useFirebaseRTDBDelegate({ firebaseApp }: { firebaseApp?: FirebaseApp }): DataDriver {
|
|
21
|
+
|
|
22
|
+
const fetchCollection = useCallback(async <M extends Record<string, any>>({
|
|
23
|
+
path,
|
|
24
|
+
filter,
|
|
25
|
+
limit,
|
|
26
|
+
startAfter,
|
|
27
|
+
orderBy,
|
|
28
|
+
order,
|
|
29
|
+
searchString
|
|
30
|
+
}: FetchCollectionProps<M>): Promise<Entity<M>[]> => {
|
|
31
|
+
if (!firebaseApp) {
|
|
32
|
+
throw new Error("Firebase app not provided");
|
|
33
|
+
}
|
|
34
|
+
const database = getDatabase(firebaseApp);
|
|
35
|
+
|
|
36
|
+
let dbQuery = query(ref(database, path));
|
|
37
|
+
|
|
38
|
+
// Example to apply "limit" and "startAfter"
|
|
39
|
+
if (startAfter !== undefined) {
|
|
40
|
+
dbQuery = query(dbQuery, orderByKey(), startAt(String(startAfter)));
|
|
41
|
+
}
|
|
42
|
+
if (limit !== undefined) {
|
|
43
|
+
dbQuery = query(dbQuery, limitToFirst(limit));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const snapshot = await get(dbQuery);
|
|
47
|
+
if (snapshot.exists()) {
|
|
48
|
+
return Object.entries(snapshot.val()).map(([id, values]) => ({
|
|
49
|
+
id,
|
|
50
|
+
path: path,
|
|
51
|
+
values: delegateToCMSModel(values) as M,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}, [firebaseApp]);
|
|
56
|
+
|
|
57
|
+
const listenCollection = useCallback(<M extends Record<string, any>>({
|
|
58
|
+
path,
|
|
59
|
+
onUpdate,
|
|
60
|
+
// Realtime Database does not directly support onError in onValue
|
|
61
|
+
}: ListenCollectionProps<M>): () => void => {
|
|
62
|
+
if (!firebaseApp) {
|
|
63
|
+
throw new Error("Firebase app not provided");
|
|
64
|
+
}
|
|
65
|
+
const database = getDatabase(firebaseApp);
|
|
66
|
+
|
|
67
|
+
const dbRef = ref(database, path);
|
|
68
|
+
const unsubscribe = onValue(dbRef, (snapshot) => {
|
|
69
|
+
if (snapshot.exists()) {
|
|
70
|
+
const result: Entity<M>[] = Object.entries(snapshot.val()).map(([id, values]) => ({
|
|
71
|
+
id,
|
|
72
|
+
path: path,
|
|
73
|
+
values: delegateToCMSModel(values) as M,
|
|
74
|
+
}));
|
|
75
|
+
onUpdate(result);
|
|
76
|
+
} else {
|
|
77
|
+
onUpdate([]);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return () => unsubscribe();
|
|
82
|
+
}, [firebaseApp]);
|
|
83
|
+
|
|
84
|
+
const fetchEntity = useCallback(async <M extends Record<string, any>>({
|
|
85
|
+
path,
|
|
86
|
+
entityId,
|
|
87
|
+
}: FetchEntityProps<M>): Promise<Entity<M> | undefined> => {
|
|
88
|
+
if (!firebaseApp) {
|
|
89
|
+
throw new Error("Firebase app not provided");
|
|
90
|
+
}
|
|
91
|
+
const database = getDatabase(firebaseApp);
|
|
92
|
+
|
|
93
|
+
const snapshot = await get(ref(database, `${path}/${entityId}`));
|
|
94
|
+
if (snapshot.exists()) {
|
|
95
|
+
return {
|
|
96
|
+
id: entityId,
|
|
97
|
+
path: path,
|
|
98
|
+
values: delegateToCMSModel(snapshot.val()) as M
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}, [firebaseApp]);
|
|
103
|
+
|
|
104
|
+
const listenEntity = useCallback(<M extends Record<string, any>>({
|
|
105
|
+
path,
|
|
106
|
+
entityId,
|
|
107
|
+
onUpdate,
|
|
108
|
+
onError
|
|
109
|
+
}: ListenEntityProps<M>): () => void => {
|
|
110
|
+
if (!firebaseApp) {
|
|
111
|
+
throw new Error("Firebase app not provided");
|
|
112
|
+
}
|
|
113
|
+
const database = getDatabase(firebaseApp);
|
|
114
|
+
|
|
115
|
+
const dbRef = ref(database, `${path}/${entityId}`);
|
|
116
|
+
const unsubscribe = onValue(dbRef, (snapshot) => {
|
|
117
|
+
if (snapshot.exists()) {
|
|
118
|
+
onUpdate({
|
|
119
|
+
id: entityId,
|
|
120
|
+
path,
|
|
121
|
+
values: delegateToCMSModel(snapshot.val()) as M
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
onError?.(new Error("Entity does not exist"));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return () => unsubscribe();
|
|
129
|
+
}, [firebaseApp]);
|
|
130
|
+
|
|
131
|
+
const saveEntity = useCallback(async <M extends Record<string, any>>({
|
|
132
|
+
path,
|
|
133
|
+
entityId,
|
|
134
|
+
values,
|
|
135
|
+
}: SaveEntityProps<M>): Promise<Entity<M>> => {
|
|
136
|
+
if (!firebaseApp) {
|
|
137
|
+
throw new Error("Firebase app not provided");
|
|
138
|
+
}
|
|
139
|
+
const database = getDatabase(firebaseApp);
|
|
140
|
+
|
|
141
|
+
// If entityId is not provided, a new entity will be created
|
|
142
|
+
const finalId = entityId ?? push(ref(database, path)).key;
|
|
143
|
+
if (!finalId) {
|
|
144
|
+
throw new Error("Could not generate a new id");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Transform the data to RTDB format before saving
|
|
148
|
+
const transformedValues = cmsToRTDBModel(values, database);
|
|
149
|
+
await set(ref(database, `${path}/${finalId}`), transformedValues);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
id: finalId,
|
|
153
|
+
path: path,
|
|
154
|
+
values: values as M
|
|
155
|
+
};
|
|
156
|
+
}, [firebaseApp]);
|
|
157
|
+
|
|
158
|
+
const deleteEntity = useCallback(async <M extends Record<string, any>>({
|
|
159
|
+
entity,
|
|
160
|
+
}: DeleteEntityProps<M>): Promise<void> => {
|
|
161
|
+
if (!firebaseApp) {
|
|
162
|
+
throw new Error("Firebase app not provided");
|
|
163
|
+
}
|
|
164
|
+
const database = getDatabase(firebaseApp);
|
|
165
|
+
|
|
166
|
+
await remove(ref(database, `${entity.path}/${entity.id}`));
|
|
167
|
+
}, [firebaseApp]);
|
|
168
|
+
|
|
169
|
+
// Implementing additional methods required by DataDriver
|
|
170
|
+
const checkUniqueField = useCallback(async (slug: string, name: string, value: any, entityId?: string | number): Promise<boolean> => {
|
|
171
|
+
if (!firebaseApp) {
|
|
172
|
+
throw new Error("Firebase app not provided");
|
|
173
|
+
}
|
|
174
|
+
const database = getDatabase(firebaseApp);
|
|
175
|
+
|
|
176
|
+
// Simplified example; the Realtime Database does not support querying with "not equal" conditions
|
|
177
|
+
const dbRef = query(ref(database, slug), orderByChild(name), startAt(value), limitToFirst(1));
|
|
178
|
+
const snapshot = await get(dbRef);
|
|
179
|
+
|
|
180
|
+
if (!snapshot.exists()) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check if the found entity is the same as the one being checked
|
|
185
|
+
const [key, entityValue] = Object.entries(snapshot.val())[0];
|
|
186
|
+
if (entityValue && typeof entityValue === "object" && (entityValue as Record<string, unknown>)[name] === value && key === entityId) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return false;
|
|
191
|
+
}, [firebaseApp]);
|
|
192
|
+
|
|
193
|
+
const isFilterCombinationValid = useCallback(({
|
|
194
|
+
path,
|
|
195
|
+
filter,
|
|
196
|
+
sortBy
|
|
197
|
+
}: any): boolean => {
|
|
198
|
+
return false;
|
|
199
|
+
}, []);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
key: "firebase_rtdb",
|
|
203
|
+
fetchCollection,
|
|
204
|
+
listenCollection,
|
|
205
|
+
fetchEntity,
|
|
206
|
+
listenEntity,
|
|
207
|
+
saveEntity,
|
|
208
|
+
deleteEntity,
|
|
209
|
+
checkUniqueField,
|
|
210
|
+
isFilterCombinationValid,
|
|
211
|
+
currentTime: () => new Date()
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Transform data from RTDB format back to CMS format
|
|
219
|
+
* This is used internally when fetching/listening to data
|
|
220
|
+
*/
|
|
221
|
+
function delegateToCMSModel(data: any): any {
|
|
222
|
+
if (data === null || data === undefined) return null;
|
|
223
|
+
|
|
224
|
+
if (Array.isArray(data)) {
|
|
225
|
+
return data.map(delegateToCMSModel).filter(v => v !== undefined);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (typeof data === "object") {
|
|
229
|
+
const result: Record<string, any> = {};
|
|
230
|
+
for (const key of Object.keys(data)) {
|
|
231
|
+
const childValue = delegateToCMSModel(data[key]);
|
|
232
|
+
if (childValue !== undefined)
|
|
233
|
+
result[key] = childValue;
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return data;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Transform data from CMS format to RTDB format
|
|
243
|
+
* This is used internally when saving data
|
|
244
|
+
*/
|
|
245
|
+
function cmsToRTDBModel(data: any, database: Database): any {
|
|
246
|
+
if (data === undefined) {
|
|
247
|
+
return null;
|
|
248
|
+
} else if (data === null) {
|
|
249
|
+
return null;
|
|
250
|
+
} else if (Array.isArray(data)) {
|
|
251
|
+
return data.filter(v => v !== undefined).map(v => cmsToRTDBModel(v, database));
|
|
252
|
+
} else if (data.isEntityReference && data.isEntityReference()) {
|
|
253
|
+
return ref(database, `${data.slug}/${data.id}`);
|
|
254
|
+
} else if (data instanceof Date) {
|
|
255
|
+
// For dates, convert to ISO string or timestamp.
|
|
256
|
+
return data.toISOString();
|
|
257
|
+
} else if (data && typeof data === "object") {
|
|
258
|
+
return Object.entries(data)
|
|
259
|
+
.map(([key, v]) => {
|
|
260
|
+
const rtdbModel = cmsToRTDBModel(v, database);
|
|
261
|
+
if (rtdbModel !== undefined)
|
|
262
|
+
return { [key]: rtdbModel };
|
|
263
|
+
else
|
|
264
|
+
return {};
|
|
265
|
+
})
|
|
266
|
+
.reduce((a, b) => ({ ...a, ...b }), {});
|
|
267
|
+
}
|
|
268
|
+
return data;
|
|
269
|
+
}
|