diff --git a/packages/realm-react/src/AppProvider.tsx b/packages/realm-react/src/AppProvider.tsx index 19a44092dde..0a8d304f21b 100644 --- a/packages/realm-react/src/AppProvider.tsx +++ b/packages/realm-react/src/AppProvider.tsx @@ -18,11 +18,20 @@ import { isEqual } from "lodash"; import React, { createContext, useContext, useLayoutEffect, useRef, useState } from "react"; import Realm from "realm"; +import { AuthResult, OperationState } from "./types"; + +type AppContextValue = { + app: Realm.App | null; + authOperationStateHook: [AuthResult, React.Dispatch>] | null; +}; /** * Create a context containing the Realm app. Should be accessed with the useApp hook. */ -const AppContext = createContext(null); +const AppContext = createContext({ + app: null, + authOperationStateHook: null, +}); /** * Props for the AppProvider component. These replicate the options which @@ -59,6 +68,13 @@ export const AppProvider: React.FC = ({ const [app, setApp] = useState(() => new Realm.App(configuration.current)); + const [authOpResult, setAuthOpResult] = useState({ + state: OperationState.NotStarted, + pending: false, + success: false, + error: undefined, + }); + // Support for a possible change in configuration if (!isEqual(appProps, configuration.current)) { configuration.current = appProps; @@ -80,7 +96,11 @@ export const AppProvider: React.FC = ({ } }, [appRef, app, logLevel]); - return {children}; + return ( + + {children} + + ); }; /** @@ -92,10 +112,21 @@ export const useApp = < FunctionsFactoryType extends Realm.DefaultFunctionsFactory, CustomDataType extends Record, >(): Realm.App => { - const app = useContext(AppContext); + const { app } = useContext(AppContext); - if (app === null) { + if (!app) { throw new Error("No app found. Did you forget to wrap your component in an ?"); } return app as Realm.App; }; + +export const useAuthResult = () => { + const { authOperationStateHook } = useContext(AppContext); + + if (!authOperationStateHook) { + throw new Error( + "Auth operation statue could not be determined. Did you forget to wrap your component in an ?", + ); + } + return authOperationStateHook; +}; diff --git a/packages/realm-react/src/useAuth.tsx b/packages/realm-react/src/useAuth.tsx index b11f94330ef..1dc214a2861 100644 --- a/packages/realm-react/src/useAuth.tsx +++ b/packages/realm-react/src/useAuth.tsx @@ -16,17 +16,18 @@ // //////////////////////////////////////////////////////////////////////////// -import { useCallback, useState } from "react"; -import { useApp } from "./AppProvider"; +import { useCallback } from "react"; +import { useApp, useAuthResult } from "./AppProvider"; import { AuthError, AuthResult, OperationState } from "./types"; import { Realm } from "realm"; +import { useAuthOperation } from "./useAuthOperation"; /** * Hook providing operations and corresponding state for authenticating with a * Realm app. * * The {@link AuthResult} states returned from this hook are "global" for all - * components under a given RealmAppProvider, as only one operation can be in progress + * components under a given AppProvider, as only one operation can be in progress * at a given time (i.e. we will store the states on the context). This means that, * for example, multiple components can use the `useAuth` hook to access * `loginResult.pending` to render a spinner when login is in progress, without @@ -108,7 +109,7 @@ interface UseAuth { /** * The {@link AuthResult} of the current (or last) login operation performed - * for this `RealmAppContext`. There is one {@link AuthResult} for all `login` + * for this hook. There is one {@link AuthResult} for all `login` * operations within a given `RealmAppProvider` context, as only one login can * be in progress at a time (e.g. the {@link AuthResult} of `loginUser` from * `useEmailPasswordAuth` is also represented by this). @@ -118,32 +119,13 @@ interface UseAuth { export function useAuth(): UseAuth { const app = useApp(); - const [result, setResult] = useState({ - state: OperationState.NotStarted, - pending: false, - success: false, - error: undefined, - }); + const [result, setResult] = useAuthResult(); - const logIn = useCallback( - (credentials: Realm.Credentials): Promise => { - if (result.state === OperationState.Pending) { - return Promise.reject("Another auth operation is already in progress."); - } - setResult({ state: OperationState.Pending, pending: true, success: false, error: undefined }); - return app.logIn(credentials).then( - (user) => { - setResult({ state: OperationState.Success, pending: false, success: true, error: undefined }); - return user; - }, - (error) => { - const authError = new AuthError(error); - setResult({ state: OperationState.Error, pending: false, success: false, error: authError }); - }, - ); - }, - [app, result, setResult], - ); + const logIn = useAuthOperation({ + result, + setResult, + operation: (credentials: Realm.Credentials) => app.logIn(credentials), + }); const logInWithAnonymous = useCallback(() => { return logIn(Realm.Credentials.anonymous()); diff --git a/packages/realm-react/src/useAuthOperation.tsx b/packages/realm-react/src/useAuthOperation.tsx new file mode 100644 index 00000000000..adba30cdb53 --- /dev/null +++ b/packages/realm-react/src/useAuthOperation.tsx @@ -0,0 +1,53 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import { useCallback } from "react"; +import { AuthError, AuthResult, OperationState } from "./types"; + +export function useAuthOperation({ + result, + setResult, + operation, + onSuccess = () => undefined, +}: { + result: AuthResult; + setResult: (value: React.SetStateAction) => void; + operation: (...args: Args) => Promise; + onSuccess?: (...args: Args) => void; +}) { + return useCallback<(...args: Args) => ReturnType>( + (...args) => { + if (result.pending) { + return Promise.reject("Another authentication operation is already in progress"); + } + + setResult({ state: OperationState.Pending, pending: true, success: false, error: undefined }); + return operation(...args) + .then((res) => { + setResult({ state: OperationState.Success, pending: false, success: true, error: undefined }); + onSuccess(...args); + return res; + }) + .catch((error) => { + const authError = new AuthError(error); + setResult({ state: OperationState.Error, pending: false, success: false, error: authError }); + }); + }, + [result, setResult, operation, onSuccess], + ); +} diff --git a/packages/realm-react/src/useEmailPasswordAuth.tsx b/packages/realm-react/src/useEmailPasswordAuth.tsx index 77cc9e7794d..aab4fed01d4 100644 --- a/packages/realm-react/src/useEmailPasswordAuth.tsx +++ b/packages/realm-react/src/useEmailPasswordAuth.tsx @@ -16,19 +16,29 @@ // //////////////////////////////////////////////////////////////////////////// -import { useCallback, useEffect, useState } from "react"; -import { useApp } from "./AppProvider"; -import { AuthError, AuthResult, OperationState } from "./types"; -import { Realm, App, Object, User, Credentials } from "realm"; - +import { useApp, useAuthResult } from "./AppProvider"; +import { AuthResult } from "./types"; +import { Realm, User, Credentials } from "realm"; +import { useAuthOperation } from "./useAuthOperation"; + +/** + * Hook providing operations and corresponding state for authenticating with a + * Realm app with Email/Password. It also contains operations related to + * Email/Password authentication, such as resetting password and confirming a user. + * + * The {@link AuthResult} states returned from this hook are "global" for all + * components under a given AppProvider, as only one operation can be in progress + * at a given time (i.e. we will store the states on the context). This means that, + * for example, multiple components can use the `useAuth` hook to access + * `result.pending` to render a spinner when login is in progress, without + * needing to pass that state around or store it somewhere global in their app + * code. + */ interface UseEmailPasswordAuth { /** * Convenience function to login a user with an email and password - users * could also call `logIn(Realm.Credentials.emailPassword(email, password)). * - * TODO: Does it make sense to have this convenience function? Should we add - * convenience functions for other/all auth types if so? - * * @returns A `Realm.User` instance for the logged in user. */ logIn(args: { email: string; password: string }): Promise; @@ -49,9 +59,6 @@ interface UseEmailPasswordAuth { loginAfterRegister?: boolean; }): Promise; - // TODO: verify that the following operations have the same error return type as - // the ones above (i.e. that `AuthResult` is the correct type for their results). - /** * Confirm a user's account by providing the `token` and `tokenId` received. * @@ -103,63 +110,20 @@ interface UseEmailPasswordAuth { /** * Log out the current user. - * - * @returns A `Realm.User` instance for the logged in user. - * @throws if another operation is already in progress for this `useAuth` instance. - * @throws if there is an error logging out. */ logOut(): Promise; /** * The {@link AuthResult} of the current (or last) operation performed for - * this `RealmAppContext`. + * this hook. */ result: AuthResult; } -function useAuthOperation({ - result, - setResult, - operation, - onSuccess = () => undefined, -}: { - result: AuthResult; - setResult: (value: React.SetStateAction) => void; - operation: (...args: Args) => Promise; - onSuccess?: (...args: Args) => void; -}) { - return useCallback<(...args: Args) => ReturnType>( - (...args) => { - if (result.pending) { - return Promise.reject("Another authentication operation is already in progress"); - } - - setResult({ state: OperationState.Pending, pending: true, success: false, error: undefined }); - return operation(...args) - .then((res) => { - setResult({ state: OperationState.Success, pending: false, success: true, error: undefined }); - onSuccess(...args); - return res; - }) - .catch((error) => { - const authError = new AuthError(error); - setResult({ state: OperationState.Error, pending: false, success: false, error: authError }); - }); - }, - [result, setResult, operation, onSuccess], - ); -} - export function useEmailPasswordAuth(): UseEmailPasswordAuth { + const [result, setResult] = useAuthResult(); const app = useApp(); - const [result, setResult] = useState({ - state: OperationState.NotStarted, - pending: false, - success: false, - error: undefined, - }); - const logIn = useAuthOperation<[{ email: string; password: string }], User>({ result, setResult,