Skip to content

Commit

Permalink
Refactorings and documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
takameyer committed Jun 7, 2023
1 parent bc600da commit 8060b0d
Show file tree
Hide file tree
Showing 11 changed files with 389 additions and 247 deletions.
31 changes: 15 additions & 16 deletions example/app/components/LoginScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ import {View, Text, StyleSheet, TextInput, Pressable} from 'react-native';
import colors from '../styles/colors';
import {shadows} from '../styles/shadows';
import {buttonStyles} from '../styles/button';
import {useAuth, useEmailPasswordAuth} from '@realm/react';
import {AuthOperationName, useAuth, useEmailPasswordAuth} from '@realm/react';

export const LoginScreen = () => {
const {result: loginResult, logInWithEmailPassword} = useAuth();
const {result: registerResult, register} = useEmailPasswordAuth();
const {result, logInWithEmailPassword} = useAuth();
const {register} = useEmailPasswordAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

const pending = loginResult.pending || registerResult.pending;

const handleLogin = useCallback(() => {
logInWithEmailPassword({email, password});
}, [logInWithEmailPassword, email, password]);
Expand Down Expand Up @@ -47,34 +45,35 @@ export const LoginScreen = () => {
/>
</View>

{loginResult.error && (
{result.error && result.error.operation === AuthOperationName.LogIn && (
<Text style={[styles.error]}>
There was an error logging in, please try again
There was an error logging in, please try again{' '}
</Text>
)}

{registerResult.error && (
<Text style={[styles.error]}>
There was an error registering, please try again
</Text>
)}
{result.error &&
result.error.operation === AuthOperationName.Register && (
<Text style={[styles.error]}>
There was an error registering, please try again
</Text>
)}

<View style={styles.buttons}>
<Pressable
onPress={handleLogin}
style={[styles.button, pending && styles.buttonDisabled]}
disabled={pending}>
style={[styles.button, result.pending && styles.buttonDisabled]}
disabled={result.pending}>
<Text style={buttonStyles.text}>Login</Text>
</Pressable>

<Pressable
onPress={handleRegister}
style={[
styles.button,
pending && styles.buttonDisabled,
result.pending && styles.buttonDisabled,
styles.registerButton,
]}
disabled={pending}>
disabled={result.pending}>
<Text style={buttonStyles.text}>Register</Text>
</Pressable>
</View>
Expand Down
4 changes: 4 additions & 0 deletions packages/realm-react/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
## vNext (TBD)

### Enhancements
* Add authentication hooks, `useAuth` and `useEmailPasswordAuth`
Usage example:
```tsx
```
* Add sync log configuration to AppProvider ([#5517](https://github.com/realm/realm-js/issue/5517))
Usage example:
```tsx
Expand Down
147 changes: 144 additions & 3 deletions packages/realm-react/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<p align="center">
<img height="140" src="media/realm-react-logo.svg" alt="Realm React Logo"/>
<img height="140" src="https://raw.githubusercontent.com/realm/realm-js/main/media/realm-react-logo.svg" alt="Realm React Logo"/>
</p>

<h1 align="center">
Expand Down Expand Up @@ -323,8 +323,149 @@ const SomeComponent = () => {
}
```

### Authentication Hooks

#### Multiple Realms
The following hooks can be used to authenticate users in your application. They return authentication operations and a single result object which can be read to track the progress of the current result. More information about the specific auth methods can be found in the [Authenticate Users](https://www.mongodb.com/docs/realm/sdk/react-native/manage-users/authenticate-users) Documentation.

## `useAuth`
These hooks would typically be used in the `fallback` component of the `UserProvider`

### `result`
The result has the following structure:
```tsx
{
/**
* The current state of the operation.
* Enumerated by OperationState
*/
state, // "not-started", "pending", "success", "error"

/**
* Convenience accessors, so users can write e.g. `loginResult.pending`
* instead of `loginResult.state === OperationState.Pending`
*/
pending, // true or false
success, // true or false

/**
* The error returned from the operation, if any. This will only be populated
* if `state === OperationState.Error`, and will be cleared each time the
* operation is called.
*/
error // Error based object or undefined
}
```

This can be used to manage the state of the current login operation.

### `logIn`
Log in with a Realm.Credentials instance. This allows login with any authentication mechanism supported by Realm. If this is called when a user is currently logged in, it will switch the user. Typically the other methods from `useAuth` would be used.
```tsx
const {logIn, result} = useAuth();
const performLogin = () => {
logIn(Realm.Credential.anonymous())
}
```

### `logInWithAnonymous`
Log in with the Anonymous authentication provider.
```tsx
const {logInWithAnonymous, result} = useAuth();
const performLogin = () => {
logInWithAnonymous();
};
```

### `logInWithApiKey`
Log in with an API key.
```tsx
const {logInWithApiKey, result} = useAuth();
const performLogin = () => {
const key = getApiKey(); // user defined function
logInWithApiKey(key);
};
```

### `logInWithEmailPassword`
Log in with Email/Password.
```tsx
const {logInWithEmailPassword, result} = useAuth();
const [email, setEmail] = useState();
const [password, setPassword] = useState();

const performLogin = () => {
logInWithEmailPassword({email, password});
};
```

### `logInWithJWT`
Log in with a JSON Web Token (JWT).
```tsx
const {logInWithJWT, result} = useAuth();

const performLogin = () => {
const token = authorizeWithCustomerProvider(); //user defined function
logInWithJWT(token);
};
```

### `logInWithGoogle`
Log in Google.
```tsx
const {logInWithGoogle, result} = useAuth();

const performLogin = () => {
const token = getGoogleToken(); //user defined function
logInWithGoogle({idToken: token});
};
```

### `logInWithApple`
Log in with Apple.
```tsx
const {logInWithApple, result} = useAuth();

const performLogin = () => {
const token = getAppleToken(); //user defined function
logInWithApple(token);
};
```

### `logInWithFacebook`
Log in with Facebook.
```tsx
const {logInWithFacebook, result} = useAuth();

const performLogin = () => {
const token = getFacebookToken(); //user defined function
logInWithFacebook(token);
};
```

### `logInWithCustomFunction`
Log in with a custom function.
```tsx
const {logInWithFunction, result} = useAuth();

const performLogin = () => {
const customPayload = getAuthParams(); // user defined arguments
logInWithFunction(customPayload);
};
```
### `logOut`
Log out the current user. This will immediately cause the `fallback` from the `UserProvider` to render.
```tsx
const {logOut, result} = useAuth();

const performLogOut = () => {
logOut();
};
```

## `useEmailPasswordAuth`
This hook is similar to `useAuth`, but specifically offers operations around Email/Password authentication. This includes methods around resetting passwords and confirming users. It returns the same `result` object as `useAuth`.

### Multiple Realms
`createRealmContext` can be used to create a contextualized hooks and a RealmProvider to the passed in configuration for a Realm. It can be called multiple times if your app requires more than one Realm. In that case, you would have multiple `RealmProvider`s that wrap your app and must use the hooks from the created context you wish to access.

The Context object will contain a `RealmProvider`, which will a open a Realm when it is rendered. It also contains a set of hooks that can be used by children to the `RealmProvider` to access and manipulate Realm data.
Expand All @@ -350,7 +491,7 @@ const { RealmProvider: PrivateRealmProvider, useRealm: usePrivateRealm, useObjec
It is also possible to call it without any Config; in the case that you want to do all your configuration through the `RealmProvider` props.


#### Sync Debug Logs
### Sync Debug Logs
When running into issues with sync, it may be helpful to view logs in order to determine what the issue was or to provide more context when submitting an issue. This can by done with the `AppProvider`.

```
Expand Down
40 changes: 28 additions & 12 deletions packages/realm-react/src/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,44 @@
import { isEqual } from "lodash";
import React, { createContext, useContext, useLayoutEffect, useRef, useState } from "react";
import Realm from "realm";
import { AuthResult, OperationState } from "./types";
import { AuthOperationName, AuthResult, OperationState } from "./types";

type AppContextValue = {
app: Realm.App | null;
authOperationStateHook: [AuthResult, React.Dispatch<React.SetStateAction<AuthResult>>] | null;
};

/**
* Create a context containing the Realm app. Should be accessed with the useApp hook.
*/
const AppContext = createContext<AppContextValue>({
app: null,
});

type AppOpStateValue = {
authOperationStateHook: [AuthResult, React.Dispatch<React.SetStateAction<AuthResult>>] | null;
};

const AuthOpStateContext = createContext<AppOpStateValue>({
authOperationStateHook: null,
});

type AuthOpStateProviderProps = { children: React.ReactNode };

const AuthOpStateProvider: React.FC<AuthOpStateProviderProps> = ({ children }) => {
const [authOpResult, setAuthOpResult] = useState<AuthResult>({
state: OperationState.NotStarted,
pending: false,
success: false,
error: undefined,
currentOperation: AuthOperationName.None,
});
return (
<AuthOpStateContext.Provider value={{ authOperationStateHook: [authOpResult, setAuthOpResult] }}>
{children}
</AuthOpStateContext.Provider>
);
};

/**
* Props for the AppProvider component. These replicate the options which
* can be used to create a Realm.App instance:
Expand Down Expand Up @@ -68,13 +91,6 @@ export const AppProvider: React.FC<AppProviderProps> = ({

const [app, setApp] = useState<Realm.App>(() => new Realm.App(configuration.current));

const [authOpResult, setAuthOpResult] = useState<AuthResult>({
state: OperationState.NotStarted,
pending: false,
success: false,
error: undefined,
});

// Support for a possible change in configuration
if (!isEqual(appProps, configuration.current)) {
configuration.current = appProps;
Expand All @@ -97,8 +113,8 @@ export const AppProvider: React.FC<AppProviderProps> = ({
}, [appRef, app, logLevel]);

return (
<AppContext.Provider value={{ app, authOperationStateHook: [authOpResult, setAuthOpResult] }}>
{children}
<AppContext.Provider value={{ app }}>
<AuthOpStateProvider>{children}</AuthOpStateProvider>
</AppContext.Provider>
);
};
Expand All @@ -121,7 +137,7 @@ export const useApp = <
};

export const useAuthResult = () => {
const { authOperationStateHook } = useContext(AppContext);
const { authOperationStateHook } = useContext(AuthOpStateContext);

if (!authOperationStateHook) {
throw new Error(
Expand Down
23 changes: 23 additions & 0 deletions packages/realm-react/src/__tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,29 @@
////////////////////////////////////////////////////////////////////////////

import { AppConfig, AppImporter, Credentials } from "@realm/app-importer";
import { act, waitFor } from "@testing-library/react-native";
import { useAuth } from "../useAuth";
import { useEmailPasswordAuth } from "../useEmailPasswordAuth";

export async function testAuthOperation({
authOperation,
result,
expectedResult,
}: {
authOperation: () => Promise<any>;
result: { current: ReturnType<typeof useEmailPasswordAuth> | ReturnType<typeof useAuth> };
expectedResult: () => void;
}) {
await act(async () => {
authOperation();
await waitFor(() => {
expect(result.current.result.pending).toEqual(true);
});
});
await waitFor(() => {
expectedResult();
});
}

const { realmBaseUrl = "http://localhost:9090" } = process.env;

Expand Down
Loading

0 comments on commit 8060b0d

Please sign in to comment.