Skip to content

Commit

Permalink
useAuth React hook (#601)
Browse files Browse the repository at this point in the history
* useAuthenticatedUser React hook
  • Loading branch information
hvergara authored Nov 11, 2021
1 parent 666122b commit f6d144d
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-eels-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aws-amplify/ui-react": patch
---

Add useAuth React hook
7 changes: 7 additions & 0 deletions docs/src/pages/ui/hooks/auth/index.page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Authentication
---

import { Fragment } from '@/components/Fragment';

<Fragment>{({ platform }) => import(`./${platform}.mdx`)}</Fragment>
37 changes: 37 additions & 0 deletions docs/src/pages/ui/hooks/auth/react.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## useAuth

Access Amplify Auth metadata using a React hook. Returns an object containing:

- `user`: `CognitoUser` user object, containing properties like `username` and `attributes`.
- `isLoading`: Indicates if metadata is being requested (`true` or `false`)
- `error`: If something fails while getting user metadata, it will contain details about the error (useful to debug issues)

Notes:

- `user` is set to `undefined` while data is being requested. Be careful to check `user` value before accessing `user.username` and `user.attributes`.
- Use `isLoading` and `error` values to provide a good user experience.

The following React component example uses `useAuth` to greet an authenticated user:

```jsx
import React from 'react';
import { useAuth, Text } from '@aws-amplify/ui-react';

const UserGreeting = () => {
const { user, isLoading, error } = useAuth();

if (isLoading) {
return <Text>Loading user details...</Text>;
}

if (error) {
return (
<Text variation="error">Error retrieving user details: {error}</Text>
);
}

return <Text>Hello, {user.username}!</Text>;
};
```

For more information about Amplify Auth, check out the [official Auth documentation](https://docs.amplify.aws/lib/auth/getting-started/q/platform/js/).
1 change: 1 addition & 0 deletions packages/react/__tests__/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1890,6 +1890,7 @@ describe('@aws-amplify/ui-react', () => {
"getOverridesFromVariants",
"primitives",
"useAmplify",
"useAuth",
"useAuthenticator",
"useDataStoreBinding",
"useDataStoreCollection",
Expand Down
132 changes: 132 additions & 0 deletions packages/react/src/hooks/__tests__/useAuth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Auth } from '@aws-amplify/auth';
import { Hub } from '@aws-amplify/core';
import { renderHook } from '@testing-library/react-hooks';
import { act } from 'react-dom/test-utils';
import { useAuth } from '../useAuth';

jest.mock('@aws-amplify/auth');

describe('useAuth', () => {
afterEach(() => jest.clearAllMocks());

it('should return default values when initialized', async () => {
(Auth.currentAuthenticatedUser as jest.Mock).mockResolvedValue(undefined);

const { result, waitForNextUpdate } = renderHook(() => useAuth());

expect(result.current.user).toBe(undefined);
expect(result.current.isLoading).toBe(true);
expect(result.current.error).toBeUndefined();

await waitForNextUpdate();
});

it('should invoke Auth.currentAuthenticatedUser function', async () => {
const mockCurrentAuthenticatedUser = jest.fn(() => Promise.resolve());

(Auth.currentAuthenticatedUser as jest.Mock).mockImplementation(
mockCurrentAuthenticatedUser
);

const { waitForNextUpdate } = renderHook(() => useAuth());

await waitForNextUpdate();

expect(mockCurrentAuthenticatedUser).toHaveBeenCalled();
});

it('should set an error when something unexpected happen', async () => {
(Auth.currentAuthenticatedUser as jest.Mock).mockRejectedValue(
new Error('Unknown error')
);

const { result, waitForNextUpdate } = renderHook(() => useAuth());

await waitForNextUpdate();

expect(result.current.error).not.toBeUndefined();
});

it('should retrieve a Cognito user', async () => {
const mockCognitoUser = {
username: 'johndoe',
attributes: {
phone_number: '+1-234-567-890',
email: 'john@doe.com',
},
};

(Auth.currentAuthenticatedUser as jest.Mock).mockResolvedValue(
mockCognitoUser
);

const { result, waitForNextUpdate } = renderHook(() => useAuth());

await waitForNextUpdate();

expect(result.current.error).toBeUndefined();
expect(result.current.user).toBe(mockCognitoUser);
});

it('should receive a Cognito user on Auth.signIn Hub event', async () => {
const mockCognitoUser = {
username: 'johndoe',
attributes: {
phone_number: '+1-234-567-890',
email: 'john@doe.com',
},
};

(Auth.currentAuthenticatedUser as jest.Mock).mockResolvedValue(undefined);

const { result, waitForNextUpdate } = renderHook(() => useAuth());

await waitForNextUpdate();

expect(result.current.user).toBe(undefined);

// Simulate Auth signIn Hub action
act(() => {
Hub.dispatch(
'auth',
{ event: 'signIn', data: mockCognitoUser },
'Auth',
Symbol.for('amplify_default')
);
});

expect(result.current.user).toBe(mockCognitoUser);
});

it('should should unset user on Auth.signOut Hub event', async () => {
const mockCognitoUser = {
username: 'johndoe',
attributes: {
phone_number: '+1-234-567-890',
email: 'john@doe.com',
},
};

(Auth.currentAuthenticatedUser as jest.Mock).mockResolvedValue(
mockCognitoUser
);

const { result, waitForNextUpdate } = renderHook(() => useAuth());

await waitForNextUpdate();

expect(result.current.user).toBe(mockCognitoUser);

// Simulate Auth signOut Hub action
act(() => {
Hub.dispatch(
'auth',
{ event: 'signOut' },
'Auth',
Symbol.for('amplify_default')
);
});

expect(result.current.user).toBeUndefined();
});
});
2 changes: 2 additions & 0 deletions packages/react/src/hooks/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { useAmplify } from './useAmplify';

export { useAuth } from './useAuth';

export {
useDataStoreBinding,
useDataStoreCollection,
Expand Down
75 changes: 75 additions & 0 deletions packages/react/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Auth, { CognitoUser } from '@aws-amplify/auth';
import { Hub } from '@aws-amplify/core';
import { useEffect, useState } from 'react';

// Exposes relevant CognitoUser properties
interface AuthUser extends CognitoUser {
username: string;
attributes: Record<string, string>;
}

export interface UseAuthResult {
user?: AuthUser;
isLoading: boolean;
error?: Error;
fetch?: () => void;
}

/**
* React hook for Amplify Auth.
* Returns a reference to current authenticated Cognito `user`.
*
* Usage:
* ```
* const { user, isLoading, error } = useAuth();
*
* if (isLoading) {
* console.info('Fetching metadata for current user');
* }
*
* if (error) {
* console.error(error.message);
* }
*
* if (user) {
* console.log(`Current username is ${user.username}`);
* }
*
* ```
*/
export const useAuth = (): UseAuthResult => {
const [result, setResult] = useState<UseAuthResult>({
error: undefined,
isLoading: true,
user: undefined,
});

const handleAuth = ({ payload }) => {
switch (payload.event) {
case 'signIn':
return setResult({ user: payload.data, isLoading: false });
case 'signOut':
return setResult({ isLoading: false });
default:
break;
}
};

const fetch = () => {
setResult({ isLoading: true });

Auth.currentAuthenticatedUser()
.then((user) => setResult({ user, isLoading: false }))
.catch((error) => setResult({ error, isLoading: false }));

// Handle Hub Auth events
Hub.listen('auth', handleAuth);

// Stop listening events on unmount
return () => Hub.remove('auth', handleAuth);
};

useEffect(fetch, []);

return { ...result, fetch };
};

0 comments on commit f6d144d

Please sign in to comment.