-
Notifications
You must be signed in to change notification settings - Fork 318
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* useAuthenticatedUser React hook
- Loading branch information
Showing
7 changed files
with
259 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@aws-amplify/ui-react": patch | ||
--- | ||
|
||
Add useAuth React hook |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
}; |