Skip to content

Commit

Permalink
[SDK-3784] Update to spa-js v2 (#432)
Browse files Browse the repository at this point in the history
  • Loading branch information
ewanharris authored Nov 15, 2022
1 parent ce0082e commit 83cf1b4
Show file tree
Hide file tree
Showing 22 changed files with 13,008 additions and 546 deletions.
124 changes: 18 additions & 106 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ const Posts = () => {
(async () => {
try {
const token = await getAccessTokenSilently({
audience: 'https://api.example.com/',
scope: 'read:posts',
authorizationParams: {
audience: 'https://api.example.com/',
scope: 'read:posts',
},
});
const response = await fetch('https://api.example.com/posts', {
headers: {
Expand All @@ -72,6 +74,7 @@ const Posts = () => {
});
setPosts(await response.json());
} catch (e) {
// Handle errors such as `login_required` and `consent_required` by re-prompting for a login
console.error(e);
}
})();
Expand Down Expand Up @@ -132,7 +135,9 @@ export default function App() {
<Auth0ProviderWithRedirectCallback
domain="YOUR_AUTH0_DOMAIN"
clientId="YOUR_AUTH0_CLIENT_ID"
redirectUri={window.location.origin}
authorizationParams={{
redirect_uri: window.location.origin,
}}
>
<Routes>
<Route path="/" exact />
Expand Down Expand Up @@ -171,8 +176,10 @@ export const wrapRootElement = ({ element }) => {
<Auth0Provider
domain="YOUR_AUTH0_DOMAIN"
clientId="YOUR_AUTH0_CLIENT_ID"
redirectUri={window.location.origin}
onRedirectCallback={onRedirectCallback}
authorizationParams={{
redirect_uri: window.location.origin,
}}
>
{element}
</Auth0Provider>
Expand Down Expand Up @@ -228,10 +235,11 @@ class MyApp extends App {
<Auth0Provider
domain="YOUR_AUTH0_DOMAIN"
clientId="YOUR_AUTH0_CLIENT_ID"
redirectUri={
typeof window !== 'undefined' ? window.location.origin : undefined
}
onRedirectCallback={onRedirectCallback}
authorizationParams={{
redirect_uri:
typeof window !== 'undefined' ? window.location.origin : undefined,
}}
>
<Component {...pageProps} />
</Auth0Provider>
Expand Down Expand Up @@ -265,104 +273,6 @@ export default withAuthenticationRequired(Profile);
See [Next.js example app](./examples/nextjs-app)
## Create a `useApi` hook for accessing protected APIs with an access token.
```js
// use-api.js
import { useEffect, useState } from 'react';
import { useAuth0 } from '@auth0/auth0-react';

export const useApi = (url, options = {}) => {
const { getAccessTokenSilently } = useAuth0();
const [state, setState] = useState({
error: null,
loading: true,
data: null,
});
const [refreshIndex, setRefreshIndex] = useState(0);

useEffect(() => {
(async () => {
try {
const { audience, scope, ...fetchOptions } = options;
const accessToken = await getAccessTokenSilently({ audience, scope });
const res = await fetch(url, {
...fetchOptions,
headers: {
...fetchOptions.headers,
// Add the Authorization header to the existing headers
Authorization: `Bearer ${accessToken}`,
},
});
setState({
...state,
data: await res.json(),
error: null,
loading: false,
});
} catch (error) {
setState({
...state,
error,
loading: false,
});
}
})();
}, [refreshIndex]);

return {
...state,
refresh: () => setRefreshIndex(refreshIndex + 1),
};
};
```
Then use it for accessing protected APIs from your components:
```jsx
// users.js
import { useApi } from './use-api';

export const Profile = () => {
const opts = {
audience: 'https://api.example.com/',
scope: 'read:users',
};
const { login, getAccessTokenWithPopup } = useAuth0();
const {
loading,
error,
refresh,
data: users,
} = useApi('https://api.example.com/users', opts);
const getTokenAndTryAgain = async () => {
await getAccessTokenWithPopup(opts);
refresh();
};
if (loading) {
return <div>Loading...</div>;
}
if (error) {
if (error.error === 'login_required') {
return <button onClick={() => login(opts)}>Login</button>;
}
if (error.error === 'consent_required') {
return (
<button onClick={getTokenAndTryAgain}>Consent to reading users</button>
);
}
return <div>Oops {error.message}</div>;
}
return (
<ul>
{users.map((user, index) => {
return <li key={index}>{user}</li>;
})}
</ul>
);
};
```
## Use with Auth0 organizations
[Organizations](https://auth0.com/docs/organizations) is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications. Note that Organizations is currently only available to customers on our Enterprise and Startup subscription plans.
Expand All @@ -375,8 +285,10 @@ ReactDOM.render(
<Auth0Provider
domain="YOUR_AUTH0_DOMAIN"
clientId="YOUR_AUTH0_CLIENT_ID"
redirectUri={window.location.origin}
organization="YOUR_ORGANIZATION_ID"
authorizationParams={{
redirectUri: window.location.origin,
}}
>
<App />
</Auth0Provider>
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ ReactDOM.render(
<Auth0Provider
domain="YOUR_AUTH0_DOMAIN"
clientId="YOUR_AUTH0_CLIENT_ID"
redirectUri={window.location.origin}
authorizationParams={{
redirect_uri: window.location.origin,
}}
>
<App />
</Auth0Provider>,
Expand Down
102 changes: 30 additions & 72 deletions __tests__/auth-provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import pkg from '../package.json';
import { createWrapper } from './helpers';
import { Auth0Provider, useAuth0 } from '../src';

const clientMock = jest.mocked(new Auth0Client({ client_id: '', domain: '' }));
const clientMock = jest.mocked(new Auth0Client({ clientId: '', domain: '' }));

describe('Auth0Provider', () => {
afterEach(() => {
Expand All @@ -35,21 +35,25 @@ describe('Auth0Provider', () => {
const opts = {
clientId: 'foo',
domain: 'bar',
redirectUri: 'baz',
maxAge: 'qux',
extra_param: '__test_extra_param__',
authorizationParams: {
redirect_uri: 'baz',
max_age: 'qux',
extra_param: '__test_extra_param__',
},
};
const wrapper = createWrapper(opts);
const { waitForNextUpdate } = renderHook(() => useContext(Auth0Context), {
wrapper,
});
expect(Auth0Client).toHaveBeenCalledWith(
expect.objectContaining({
client_id: 'foo',
clientId: 'foo',
domain: 'bar',
redirect_uri: 'baz',
max_age: 'qux',
extra_param: '__test_extra_param__',
authorizationParams: {
redirect_uri: 'baz',
max_age: 'qux',
extra_param: '__test_extra_param__',
},
})
);
await waitForNextUpdate();
Expand Down Expand Up @@ -228,41 +232,6 @@ describe('Auth0Provider', () => {
expect(result.current.error).not.toBeDefined();
});

it('should call through to buildAuthorizeUrl method', async () => {
const wrapper = createWrapper();
const { waitForNextUpdate, result } = renderHook(
() => useContext(Auth0Context),
{ wrapper }
);
await waitForNextUpdate();
expect(result.current.buildAuthorizeUrl).toBeInstanceOf(Function);

await result.current.buildAuthorizeUrl({
redirectUri: '__redirect_uri__',
});
expect(clientMock.buildAuthorizeUrl).toHaveBeenCalledWith({
redirect_uri: '__redirect_uri__',
});
});

it('should call through to buildLogoutUrl method', async () => {
const wrapper = createWrapper();
const { waitForNextUpdate, result } = renderHook(
() => useContext(Auth0Context),
{ wrapper }
);
await waitForNextUpdate();
expect(result.current.buildLogoutUrl).toBeInstanceOf(Function);

const logoutOptions = {
returnTo: '/',
client_id: 'blah',
federated: false,
};
result.current.buildLogoutUrl(logoutOptions);
expect(clientMock.buildLogoutUrl).toHaveBeenCalledWith(logoutOptions);
});

it('should login with a popup', async () => {
clientMock.getUser.mockResolvedValue(undefined);
const wrapper = createWrapper();
Expand Down Expand Up @@ -320,10 +289,14 @@ describe('Auth0Provider', () => {
await waitForNextUpdate();
expect(result.current.loginWithRedirect).toBeInstanceOf(Function);
await result.current.loginWithRedirect({
redirectUri: '__redirect_uri__',
authorizationParams: {
redirect_uri: '__redirect_uri__',
},
});
expect(clientMock.loginWithRedirect).toHaveBeenCalledWith({
redirect_uri: '__redirect_uri__',
authorizationParams: {
redirect_uri: '__redirect_uri__',
},
});
});

Expand All @@ -346,28 +319,7 @@ describe('Auth0Provider', () => {
expect(result.current.user).toBe(user);
});

it('should update state for local logouts', async () => {
const user = { name: '__test_user__' };
clientMock.getUser.mockResolvedValue(user);
const wrapper = createWrapper();
const { waitForNextUpdate, result } = renderHook(
() => useContext(Auth0Context),
{ wrapper }
);
await waitForNextUpdate();
expect(result.current.isAuthenticated).toBe(true);
expect(result.current.user).toBe(user);
act(() => {
result.current.logout({ localOnly: true });
});
expect(clientMock.logout).toHaveBeenCalledWith({
localOnly: true,
});
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.user).toBeUndefined();
});

it('should update state for local logouts with async cache', async () => {
it('should update state when using onRedirect', async () => {
const user = { name: '__test_user__' };
clientMock.getUser.mockResolvedValue(user);
// get logout to return a Promise to simulate async cache.
Expand All @@ -380,7 +332,8 @@ describe('Auth0Provider', () => {
await waitForNextUpdate();
expect(result.current.isAuthenticated).toBe(true);
await act(async () => {
await result.current.logout({ localOnly: true });
// eslint-disable-next-line @typescript-eslint/no-empty-function
await result.current.logout({ onRedirect: async () => {} });
});
expect(result.current.isAuthenticated).toBe(false);
});
Expand Down Expand Up @@ -895,15 +848,20 @@ describe('Auth0Provider', () => {
wrapper,
});

await expect(
auth0ContextRender.result.current.getIdTokenClaims
).toThrowError('You forgot to wrap your component in <Auth0Provider>.');
await act(async () => {
await expect(
auth0ContextRender.result.current.getIdTokenClaims
).toThrowError('You forgot to wrap your component in <Auth0Provider>.');
});

const customContextRender = renderHook(() => useContext(context), {
wrapper,
});

const claims = await customContextRender.result.current.getIdTokenClaims();
let claims;
await act(async () => {
claims = await customContextRender.result.current.getIdTokenClaims();
});
expect(clientMock.getIdTokenClaims).toHaveBeenCalled();
expect(claims).toStrictEqual({
claim: '__test_claim__',
Expand Down
2 changes: 1 addition & 1 deletion __tests__/with-authentication-required.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Auth0Client, User } from '@auth0/auth0-spa-js';
import Auth0Provider from '../src/auth0-provider';
import { Auth0ContextInterface, initialContext } from '../src/auth0-context';

const mockClient = jest.mocked(new Auth0Client({ client_id: '', domain: '' }));
const mockClient = jest.mocked(new Auth0Client({ clientId: '', domain: '' }));

describe('withAuthenticationRequired', () => {
it('should block access to a private component when not authenticated', async () => {
Expand Down
4 changes: 3 additions & 1 deletion examples/cra-react-router/src/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export function Nav() {
<button
className="btn btn-outline-secondary"
id="logout"
onClick={() => logout({ returnTo: window.location.origin })}
onClick={() =>
logout({ logoutParams: { returnTo: window.location.origin } })
}
>
logout
</button>
Expand Down
15 changes: 8 additions & 7 deletions examples/cra-react-router/src/Users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { Error } from './Error';
const PORT = process.env.REACT_APP_API_PORT || 3001;

export function Users() {
const { loading, error, data: users = [] } = useApi(
`http://localhost:${PORT}/users`,
{
audience: process.env.REACT_APP_AUDIENCE,
scope: 'read:users',
}
);
const {
loading,
error,
data: users = [],
} = useApi(`http://localhost:${PORT}/users`, {
audience: process.env.REACT_APP_AUDIENCE,
scope: 'profile email read:users',
});

if (loading) {
return <Loading />;
Expand Down
Loading

0 comments on commit 83cf1b4

Please sign in to comment.