Skip to content

Commit

Permalink
Merge b90e256 into 4a54dce
Browse files Browse the repository at this point in the history
  • Loading branch information
jkdowdle committed Jan 23, 2024
2 parents 4a54dce + b90e256 commit 060d8ce
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 13 deletions.
9 changes: 7 additions & 2 deletions src/common/RootProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import { AuthConfiguration } from 'react-native-app-auth';
import { AuthContextProvider } from '../hooks/useAuth';
import { HttpClientContextProvider } from '../hooks/useHttpClient';
import { OAuthContextProvider } from '../hooks/useOAuthFlow';
import { ModifyAuthConfig, OAuthContextProvider } from '../hooks/useOAuthFlow';
import { GraphQLClientContextProvider } from '../hooks/useGraphQLClient';
import { useDeveloperConfig } from '../hooks/useDeveloperConfig';
import { BrandConfigProvider } from '../components/BrandConfigProvider';
Expand All @@ -21,12 +21,14 @@ const queryClient = new QueryClient();
export type RootProvidersProps = {
account: string;
authConfig: AuthConfiguration;
modifyAuthConfig?: ModifyAuthConfig;
children?: React.ReactNode;
};

export function RootProviders({
account,
authConfig,
modifyAuthConfig,
children,
}: RootProvidersProps) {
const { apiBaseURL, theme, brand } = useDeveloperConfig();
Expand All @@ -37,7 +39,10 @@ export function RootProviders({
<AuthContextProvider>
<HttpClientContextProvider baseURL={apiBaseURL}>
<GraphQLClientContextProvider baseURL={apiBaseURL}>
<OAuthContextProvider authConfig={authConfig}>
<OAuthContextProvider
authConfig={authConfig}
modifyAuthConfig={modifyAuthConfig}
>
<BrandConfigProvider theme={theme} {...brand}>
<NoInternetToastProvider>
<ActionSheetProvider>
Expand Down
106 changes: 103 additions & 3 deletions src/hooks/useOAuthFlow.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React from 'react';
import { renderHook, act } from '@testing-library/react-native';
import { authorize, refresh, revoke } from 'react-native-app-auth';
import { OAuthContextProvider, useOAuthFlow } from './useOAuthFlow';
import {
ModifyAuthConfig,
OAuthContextProvider,
useOAuthFlow,
} from './useOAuthFlow';
import { AuthResult, useAuth } from './useAuth';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';

Expand Down Expand Up @@ -42,11 +46,16 @@ const authResult: AuthResult = {
refreshToken: 'refreshToken',
};

const renderHookInContext = async () => {
const renderHookInContext = async ({
modifyAuthConfig,
}: { modifyAuthConfig?: ModifyAuthConfig } = {}) => {
return renderHook(() => useOAuthFlow(), {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
<OAuthContextProvider authConfig={authConfig}>
<OAuthContextProvider
authConfig={authConfig}
modifyAuthConfig={modifyAuthConfig}
>
{children}
</OAuthContextProvider>
</QueryClientProvider>
Expand All @@ -66,6 +75,23 @@ beforeEach(() => {
authorizeMock.mockResolvedValue(authResult);
});

const modifyAuthConfig: ModifyAuthConfig = (
action,
config,
previousAuthResult,
) => {
return {
...config,
customHeaders: {
action,
authResult: previousAuthResult
? previousAuthResult
: 'no-previous-auth-result',
...config.customHeaders,
},
};
};

test('without provider, methods fail', async () => {
const { result } = renderHook(() => useOAuthFlow());
await expect(result.current.login({})).rejects.toBeUndefined();
Expand Down Expand Up @@ -124,6 +150,32 @@ describe('login', () => {
expect(clearAuthResultMock).toHaveBeenCalled();
expect(onFail).toHaveBeenCalledWith(error);
});

test('allows for customizing auth config used for login', async () => {
useAuthMock.mockReturnValueOnce({
isLoggedIn: false,
authResult: undefined,
storeAuthResult: storeAuthResultMock,
clearAuthResult: clearAuthResultMock,
initialize: useAuthInitialize,
});

const { result } = await renderHookInContext({ modifyAuthConfig });
expect(useAuthInitialize).toHaveBeenCalled();

await act(async () => {
await result.current.login({});
});

expect(authorize).toHaveBeenCalledWith({
...authConfig,
customHeaders: {
// values set by provided modifyAuthConfig
authResult: 'no-previous-auth-result',
action: 'authorize',
},
});
});
});

describe('logout', () => {
Expand All @@ -141,6 +193,20 @@ describe('logout', () => {
expect(onSuccess).toHaveBeenCalledWith();
});

test('allows for customizing auth config used for logout', async () => {
const { result } = await renderHookInContext({ modifyAuthConfig });
expect(useAuthInitialize).toHaveBeenCalled();

await act(async () => {
await result.current.logout({});
});

expect(revoke).toHaveBeenCalledWith(
{ ...authConfig, customHeaders: { action: 'revoke', authResult } },
{ sendClientId: true, tokenToRevoke: authResult.refreshToken },
);
});

test('upon error, still clears storage and reports error', async () => {
const { result } = await renderHookInContext();
const onFail = jest.fn();
Expand Down Expand Up @@ -176,6 +242,23 @@ describe('logout', () => {
expect(clearAuthResultMock).toHaveBeenCalled();
expect(onSuccess).toHaveBeenCalledWith();
});

test('allows for customizing auth config used for refresh handler', async () => {
await renderHookInContext({ modifyAuthConfig });
expect(useAuthInitialize).toHaveBeenCalled();

const refreshHandler = useAuthInitialize.mock.calls[0][0];
await act(async () => {
await refreshHandler({ refreshToken: 'refresh-token' });
});

expect(refreshMock).toHaveBeenCalledWith(
{ ...authConfig, customHeaders: { action: 'refreshToken', authResult } },
{
refreshToken: 'refresh-token',
},
);
});
});

describe('refreshHandler', () => {
Expand All @@ -202,4 +285,21 @@ describe('refreshHandler', () => {
await expect(refreshHandler).rejects.toThrow();
});
});

test('allows for customizing auth config used for refresh handler', async () => {
await renderHookInContext({ modifyAuthConfig });
expect(useAuthInitialize).toHaveBeenCalled();

const refreshHandler = useAuthInitialize.mock.calls[0][0];
await act(async () => {
await refreshHandler({ refreshToken: 'refresh-token' });
});

expect(refreshMock).toHaveBeenCalledWith(
{ ...authConfig, customHeaders: { action: 'refreshToken', authResult } },
{
refreshToken: 'refresh-token',
},
);
});
});
62 changes: 54 additions & 8 deletions src/hooks/useOAuthFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,48 @@ export interface LogoutParams {
onFail?: (error?: any) => void;
}

/**
* Callback function to modify the authentication configuration during OAuth
* flows.
*
* While typically not needed, this function is invoked when the application
* performs oauth actions like authorize on login or revoke on logout etc.,
* providing an opportunity to dynamically change the authentication
* configuration used for those actions.
*
* @example
* <RootProviders
* account="fountainlife"
* authConfig={authConfig}
* modifyAuthConfig={(action, config, previousAuthResult) => {
* if (action === 'refreshToken') {
* return {
* ...config,
* // make auth config change
* }
* }
* return config
* }}
* />
*/
export type ModifyAuthConfig = (
action: 'authorize' | 'refreshToken' | 'revoke',
currentAuthConfig: AuthConfiguration,
previousAuthorizeResult?: AuthResult,
) => AuthConfiguration;

const OAuthContext = createContext<OAuthConfig>({
login: (_) => Promise.reject(),
logout: () => Promise.reject(),
});

export const OAuthContextProvider = ({
authConfig,
modifyAuthConfig = () => authConfig,
children,
}: {
authConfig: AuthConfiguration;
modifyAuthConfig?: ModifyAuthConfig;
children?: React.ReactNode;
}) => {
const {
Expand Down Expand Up @@ -99,7 +131,7 @@ export const OAuthContextProvider = ({
}

try {
await revoke(authConfig, {
await revoke(modifyAuthConfig('revoke', authConfig, authResult), {
tokenToRevoke: authResult.refreshToken,
sendClientId: true,
});
Expand All @@ -113,9 +145,10 @@ export const OAuthContextProvider = ({
[
queryClient,
isLoggedIn,
authResult?.refreshToken,
authResult,
clearAuthResult,
authConfig,
modifyAuthConfig,
],
);

Expand All @@ -124,7 +157,9 @@ export const OAuthContextProvider = ({
const { onSuccess, onFail } = params;

try {
const result = await authorize(authConfig);
const result = await authorize(
modifyAuthConfig('authorize', authConfig, authResult),
);
await storeAuthResult(result);
_sdkAnalyticsEvent.track('Login', { usedInvite: !!pendingInvite?.evc });
onSuccess?.(result);
Expand All @@ -133,7 +168,14 @@ export const OAuthContextProvider = ({
onFail?.(error);
}
},
[authConfig, clearAuthResult, pendingInvite?.evc, storeAuthResult],
[
authConfig,
clearAuthResult,
pendingInvite?.evc,
storeAuthResult,
modifyAuthConfig,
authResult,
],
);

const refreshHandler = useCallback(
Expand All @@ -143,11 +185,15 @@ export const OAuthContextProvider = ({
'No refreshToken! The app can NOT function properly without a refreshToken. Expect to be logged out immediately.',
);
}
return await refresh(authConfig, {
refreshToken: storedResult.refreshToken,
});

return await refresh(
modifyAuthConfig('refreshToken', authConfig, authResult),
{
refreshToken: storedResult.refreshToken,
},
);
},
[authConfig],
[authConfig, modifyAuthConfig, authResult],
);

useEffect(() => {
Expand Down

0 comments on commit 060d8ce

Please sign in to comment.