Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDK-1642] Add missing methods from SPA JS #11

Merged
merged 2 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions __mocks__/@auth0/auth0-spa-js.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
export const handleRedirectCallback = jest.fn(() => ({ appState: {} }));
export const getTokenSilently = jest.fn();
export const getTokenWithPopup = jest.fn();
export const getUser = jest.fn();
export const getIdTokenClaims = jest.fn();
export const isAuthenticated = jest.fn(() => false);
export const loginWithPopup = jest.fn();
export const loginWithRedirect = jest.fn();
export const logout = jest.fn();

export const Auth0Client = jest.fn(() => {
return {
handleRedirectCallback,
getTokenSilently,
getTokenWithPopup,
getUser,
getIdTokenClaims,
isAuthenticated,
loginWithPopup,
loginWithRedirect,
logout,
};
Expand Down
80 changes: 79 additions & 1 deletion __tests__/auth-provider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { useContext } from 'react';
import Auth0Context from '../src/auth0-context';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react-hooks';
import {
Auth0Client,
// @ts-ignore
getTokenSilently,
// @ts-ignore
getTokenWithPopup,
// @ts-ignore
isAuthenticated,
// @ts-ignore
getUser,
// @ts-ignore
getIdTokenClaims,
// @ts-ignore
handleRedirectCallback,
// @ts-ignore
loginWithPopup,
// @ts-ignore
loginWithRedirect,
// @ts-ignore
logout,
Expand Down Expand Up @@ -148,6 +154,50 @@ describe('Auth0Provider', () => {
expect(onRedirectCallback).toHaveBeenCalledWith({ foo: 'bar' });
});

it('should login with a popup', async () => {
isAuthenticated.mockResolvedValue(false);
const wrapper = createWrapper();
const { waitForNextUpdate, result } = renderHook(
() => useContext(Auth0Context),
{ wrapper }
);
await waitForNextUpdate();
expect(result.current.isAuthenticated).toBe(false);
isAuthenticated.mockResolvedValue(true);
act(() => {
result.current.loginWithPopup();
});
expect(result.current.isLoading).toBe(true);
await waitForNextUpdate();
expect(result.current.isLoading).toBe(false);
expect(loginWithPopup).toHaveBeenCalled();
expect(result.current.isAuthenticated).toBe(true);
});

it('should handle errors when logging in with a popup', async () => {
isAuthenticated.mockResolvedValue(false);
const wrapper = createWrapper();
const { waitForNextUpdate, result } = renderHook(
() => useContext(Auth0Context),
{ wrapper }
);
await waitForNextUpdate();
expect(result.current.isAuthenticated).toBe(false);
isAuthenticated.mockResolvedValue(false);
loginWithPopup.mockRejectedValue(new Error('__test_error__'));
act(() => {
result.current.loginWithPopup();
});
expect(result.current.isLoading).toBe(true);
await waitForNextUpdate();
expect(result.current.isLoading).toBe(false);
expect(loginWithPopup).toHaveBeenCalled();
expect(result.current.isAuthenticated).toBe(false);
expect(() => {
throw result.current.error;
}).toThrowError('__test_error__');
});

it('should provide a login method', async () => {
const wrapper = createWrapper();
const { waitForNextUpdate, result } = renderHook(
Expand Down Expand Up @@ -189,4 +239,32 @@ describe('Auth0Provider', () => {
expect(getTokenSilently).toHaveBeenCalled();
expect(token).toBe('token');
});

it('should provide a getTokenWithPopup method', async () => {
getTokenWithPopup.mockResolvedValue('token');
const wrapper = createWrapper();
const { waitForNextUpdate, result } = renderHook(
() => useContext(Auth0Context),
{ wrapper }
);
await waitForNextUpdate();
expect(result.current.getTokenWithPopup).toBeInstanceOf(Function);
const token = await result.current.getTokenWithPopup();
expect(getTokenWithPopup).toHaveBeenCalled();
expect(token).toBe('token');
});

it('should provide a getIdTokenClaims method', async () => {
getIdTokenClaims.mockResolvedValue({ claim: '__test_claim__' });
const wrapper = createWrapper();
const { waitForNextUpdate, result } = renderHook(
() => useContext(Auth0Context),
{ wrapper }
);
await waitForNextUpdate();
expect(result.current.getIdTokenClaims).toBeInstanceOf(Function);
const claims = await result.current.getIdTokenClaims();
expect(getIdTokenClaims).toHaveBeenCalled();
expect(claims).toStrictEqual({ claim: '__test_claim__' });
});
});
2 changes: 1 addition & 1 deletion __tests__/utils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('utils defaultOnRedirectCallback', () => {
expect(window.location.href).toBe(
'https://www.example.com/?code=__test_code__&state=__test_state__'
);
defaultOnRedirectCallback({ redirectTo: '/foo' });
defaultOnRedirectCallback({ returnTo: '/foo' });
expect(window.location.href).toBe('https://www.example.com/foo');
});
});
Expand Down
22 changes: 22 additions & 0 deletions src/auth0-context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import {
GetIdTokenClaimsOptions,
GetTokenSilentlyOptions,
GetTokenWithPopupOptions,
IdToken,
LogoutOptions,
PopupLoginOptions,
RedirectLoginOptions,
} from '@auth0/auth0-spa-js';
import { createContext } from 'react';
Expand All @@ -12,11 +16,26 @@ export interface Auth0ContextInterface extends AuthState {
*/
getToken: (options?: GetTokenSilentlyOptions) => Promise<string>;

/**
* Get an access token interactively.
*/
getTokenWithPopup: (options?: GetTokenWithPopupOptions) => Promise<string>;

/**
* Returns all claims from the id_token if available.
*/
getIdTokenClaims: (options?: GetIdTokenClaimsOptions) => Promise<IdToken>;

/**
* Login in with a redirect.
*/
login: (options?: RedirectLoginOptions) => Promise<void>;

/**
* Login in with a popup.
*/
loginWithPopup: (options?: PopupLoginOptions) => Promise<void>;

/**
* Logout.
*/
Expand All @@ -30,6 +49,9 @@ const stub = (): never => {
export default createContext<Auth0ContextInterface>({
...initialAuthState,
getToken: stub,
getTokenWithPopup: stub,
getIdTokenClaims: stub,
login: stub,
loginWithPopup: stub,
logout: stub,
});
24 changes: 23 additions & 1 deletion src/auth0-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import React, {
useReducer,
useState,
} from 'react';
import { Auth0Client, Auth0ClientOptions } from '@auth0/auth0-spa-js';
import {
Auth0Client,
Auth0ClientOptions,
IdToken,
PopupLoginOptions,
} from '@auth0/auth0-spa-js';
import Auth0Context from './auth0-context';
import {
AppState,
Expand Down Expand Up @@ -50,12 +55,29 @@ const Auth0Provider = ({
})();
}, [client, onRedirectCallback]);

const loginWithPopup = async (options?: PopupLoginOptions): Promise<void> => {
dispatch({ type: 'LOGIN_POPUP_STARTED' });
try {
await client.loginWithPopup(options);
} catch (error) {
dispatch({ type: 'ERROR', error: loginError(error) });
}
const isAuthenticated = await client.isAuthenticated();
const user = isAuthenticated && (await client.getUser());
dispatch({ type: 'LOGIN_POPUP_COMPLETE', isAuthenticated, user });
};

return (
<Auth0Context.Provider
value={{
...state,
getToken: (opts): Promise<string> => client.getTokenSilently(opts),
getTokenWithPopup: (opts): Promise<string> =>
client.getTokenWithPopup(opts),
getIdTokenClaims: (opts): Promise<IdToken> =>
client.getIdTokenClaims(opts),
login: (opts): Promise<void> => client.loginWithRedirect(opts),
loginWithPopup: (opts): Promise<void> => loginWithPopup(opts),
logout: (opts): void => client.logout(opts),
}}
>
Expand Down
13 changes: 12 additions & 1 deletion src/reducer.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { AuthState } from './auth-state';

type Action =
| { type: 'INITIALISED'; isAuthenticated: boolean; user?: unknown }
| { type: 'LOGIN_POPUP_STARTED' }
| {
type: 'INITIALISED' | 'LOGIN_POPUP_COMPLETE';
isAuthenticated: boolean;
user?: unknown;
}
| { type: 'ERROR'; error: Error };

export const reducer = (state: AuthState, action: Action): AuthState => {
switch (action.type) {
case 'LOGIN_POPUP_STARTED':
return {
...state,
isLoading: true,
};
case 'LOGIN_POPUP_COMPLETE':
case 'INITIALISED':
return {
...state,
Expand Down
4 changes: 2 additions & 2 deletions src/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const CODE_RE = /[?&]code=[^&]+/;
const ERROR_RE = /[?&]error=[^&]+/;

export type AppState = {
redirectTo?: string;
returnTo?: string;
[key: string]: unknown;
};

Expand All @@ -13,7 +13,7 @@ export const defaultOnRedirectCallback = (appState?: AppState): void => {
window.history.replaceState(
{},
document.title,
appState?.redirectTo || window.location.pathname
appState?.returnTo || window.location.pathname
);
};

Expand Down
2 changes: 1 addition & 1 deletion src/with-login-required.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const withLoginRequired = <P extends object>(
}
(async (): Promise<void> => {
await login({
appState: { redirectTo: window.location.pathname },
appState: { returnTo: window.location.pathname },
});
})();
}, [isLoading, isAuthenticated, login]);
Expand Down
36 changes: 33 additions & 3 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,48 @@
<script src="auth0-react.js"></script>
<script type="text/babel">
const { Auth0Provider, useAuth0 } = reactAuth0;
const { useState } = React;

const App = () => {
const { isAuthenticated, isLoading, login, logout } = useAuth0();
const {
isAuthenticated,
isLoading,
getIdTokenClaims,
getToken,
getTokenWithPopup,
login,
loginWithPopup,
logout,
} = useAuth0();
const [token, setToken] = useState(null);
const [claims, setClaims] = useState(null);

if (isLoading) {
return <div>loading...</div>;
}

return isAuthenticated ? (
<button onClick={() => logout()}>logout</button>
<div>
<button onClick={() => logout()}>logout</button>
<button onClick={async () => setToken(await getToken())}>
Get token
</button>
<button onClick={async () => setToken(await getTokenWithPopup())}>
Get token with popup
</button>
<button onClick={async () => setClaims(await getIdTokenClaims())}>
Get id_token claims
</button>
{token && <pre>access_token: {token}</pre>}
{claims && (
<pre>id_token_claims: {JSON.stringify(claims, null, 2)}</pre>
)}
</div>
) : (
<button onClick={() => login()}>Login</button>
<div>
<button onClick={() => login()}>Login</button>
<button onClick={() => loginWithPopup()}>Login with popup</button>
</div>
);
};

Expand Down