Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
181 changes: 24 additions & 157 deletions apps/studio/src/components/auth-provider.tsx
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove this context provider altogether. Components can easily consume Redux state and dispatch actions. There's no point in keeping this provider as a thin wrapper around the Redux slice.

Original file line number Diff line number Diff line change
@@ -1,33 +1,31 @@
import * as Sentry from '@sentry/electron/renderer';
import wpcomFactory from '@studio/common/lib/wpcom-factory';
import wpcomXhrRequest from '@studio/common/lib/wpcom-xhr-request-factory';
import { useI18n } from '@wordpress/react-i18n';
import { createContext, useState, useEffect, useMemo, useCallback, ReactNode } from 'react';
import { createContext, useCallback, useEffect, useMemo, ReactNode } from 'react';
import { useIpcListener } from 'src/hooks/use-ipc-listener';
import { useOffline } from 'src/hooks/use-offline';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { isInvalidTokenError } from 'src/lib/is-invalid-oauth-token-error';
import { useI18nLocale } from 'src/stores';
import { setWpcomClient } from 'src/stores/wpcom-api';
import { useAppDispatch, useRootSelector, useI18nLocale } from 'src/stores';
import {
authLogout,
authTokenReceived,
initializeAuth,
selectIsAuthenticated,
selectUser,
} from 'src/stores/auth-slice';
import { getWpcomClient } from 'src/stores/wpcom-api';
import type { WPCOM } from 'wpcom/types';

export interface AuthContextType {
client: WPCOM | undefined;
isAuthenticated: boolean;
authenticate: () => void; // Adjust based on the actual implementation
logout: () => Promise< void >; // Adjust based on the actual implementation
authenticate: () => void;
logout: () => Promise< void >;
user?: { id: number; email: string; displayName: string };
}

interface AuthProviderProps {
children: ReactNode;
}

interface WpcomParams extends Record< string, unknown > {
query?: string;
apiNamespace?: string;
}

export const AuthContext = createContext< AuthContextType >( {
client: undefined,
isAuthenticated: false,
Expand All @@ -38,35 +36,25 @@ export const AuthContext = createContext< AuthContextType >( {
} );

const AuthProvider: React.FC< AuthProviderProps > = ( { children } ) => {
const [ isAuthenticated, setIsAuthenticated ] = useState( false );
const [ client, setClient ] = useState< WPCOM | undefined >( undefined );
const [ user, setUser ] = useState< AuthContextType[ 'user' ] >( undefined );
const dispatch = useAppDispatch();
const locale = useI18nLocale();
const { __ } = useI18n();
const isOffline = useOffline();

const isAuthenticated = useRootSelector( selectIsAuthenticated );
const user = useRootSelector( selectUser );

const authenticate = useCallback( () => getIpcApi().authenticate(), [] );

const handleInvalidToken = useCallback( async () => {
try {
void getIpcApi().logRendererMessage( 'info', 'Detected invalid token. Logging out.' );
await getIpcApi().clearAuthenticationToken();
setIsAuthenticated( false );
setClient( undefined );
setWpcomClient( undefined );
setUser( undefined );
} catch ( err ) {
console.error( 'Failed to handle invalid token:', err );
Sentry.captureException( err );
}
}, [] );
useEffect( () => {
void dispatch( initializeAuth( { locale } ) );
}, [ dispatch, locale ] );

useIpcListener( 'auth-updated', ( _event, payload ) => {
if ( 'error' in payload ) {
let title: string = __( 'Authentication error' );
let message: string = __( 'Please try again.' );

// User has denied access to the authorization dialog.
if ( payload.error instanceof Error && payload.error.message.includes( 'access_denied' ) ) {
title = __( 'Authorization denied' );
message = __(
Expand All @@ -78,146 +66,25 @@ const AuthProvider: React.FC< AuthProviderProps > = ( { children } ) => {
return;
}

const { token } = payload;
const newClient = createWpcomClient( token.accessToken, locale, handleInvalidToken );

setIsAuthenticated( true );
setClient( newClient );
setWpcomClient( newClient );
setUser( {
id: token.id,
email: token.email,
displayName: token.displayName || '',
} );
void dispatch( authTokenReceived( { token: payload.token, locale } ) );
} );

const logout = useCallback( async () => {
if ( ! isOffline && client ) {
try {
await client.req.del( {
apiNamespace: 'wpcom/v2',
path: '/studio-app/token',
// wpcom.req.del defaults to POST; explicitly send HTTP DELETE for v2
method: 'DELETE',
} );
console.log( 'Token revoked' );
} catch ( err ) {
console.error( 'Failed to revoke token:', err );
Sentry.captureException( err );
}
} else if ( isOffline ) {
console.log( 'Offline: Skipping token revocation request' );
}

try {
await getIpcApi().clearAuthenticationToken();
setIsAuthenticated( false );
setClient( undefined );
setWpcomClient( undefined );
setUser( undefined );
} catch ( err ) {
console.error( err );
Sentry.captureException( err );
}
}, [ client, isOffline ] );

useEffect( () => {
async function run() {
try {
const token = await getIpcApi().getAuthenticationToken();

if ( ! token ) {
setIsAuthenticated( false );
return;
}

const newClient = createWpcomClient( token.accessToken, locale, handleInvalidToken );
await dispatch( authLogout( { isOffline } ) );
}, [ dispatch, isOffline ] );

setIsAuthenticated( true );
setClient( newClient );
setWpcomClient( newClient );
setUser( {
id: token.id,
email: token.email,
displayName: token.displayName || '',
} );
} catch ( err ) {
console.error( err );
Sentry.captureException( err );
}
}
void run();
}, [ locale, handleInvalidToken ] );

// Memoize the context value to avoid unnecessary renders
const contextValue: AuthContextType = useMemo(
() => ( {
client,
client: getWpcomClient(),
isAuthenticated,
authenticate,
logout,
user,
} ),
[ client, isAuthenticated, authenticate, logout, user ]
[ isAuthenticated, authenticate, logout, user ]
);

return <AuthContext.Provider value={ contextValue }>{ children }</AuthContext.Provider>;
};

function createWpcomClient(
token?: string,
locale?: string,
onInvalidToken?: () => Promise< void >
): WPCOM {
let isAuthErrorDialogOpen = false;
const handleInvalidTokenError = async ( response: unknown ) => {
if ( isInvalidTokenError( response ) && onInvalidToken && ! isAuthErrorDialogOpen ) {
isAuthErrorDialogOpen = true;
await onInvalidToken();
await getIpcApi().showMessageBox( {
type: 'error',
message: 'Session Expired',
detail: 'Your session has expired. Please log in again.',
} );
isAuthErrorDialogOpen = false;
}
};

const addLocaleToParams = ( params: WpcomParams ) => {
if ( locale && locale !== 'en' ) {
const queryParams = new URLSearchParams(
'query' in params && typeof params.query === 'string' ? params.query : ''
);
const localeParamName =
'apiNamespace' in params && typeof params.apiNamespace === 'string' ? '_locale' : 'locale';
queryParams.set( localeParamName, locale );

Object.assign( params, {
query: queryParams.toString(),
} );
}
return params;
};

// Wrap the request handler to add locale and error handling before passing to wpcomFactory
const wrappedRequestHandler = (
params: object,
callback: ( err: unknown, response?: unknown, headers?: unknown ) => void
) => {
const modifiedParams = addLocaleToParams( params as WpcomParams );
const wrappedCallback = ( err: unknown, response: unknown, headers: unknown ) => {
if ( err ) {
void handleInvalidTokenError( err );
}
if ( typeof callback === 'function' ) {
callback( err, response, headers );
}
};

return wpcomXhrRequest( modifiedParams, wrappedCallback );
};

return wpcomFactory( token, wrappedRequestHandler );
}

export default AuthProvider;
13 changes: 7 additions & 6 deletions apps/studio/src/components/tests/content-tab-assistant.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import nock from 'nock';
import { Provider } from 'react-redux';
import { Dispatch } from 'redux';
import { vi } from 'vitest';
import { AuthContext, AuthContextType } from 'src/components/auth-provider';
import { AuthContextType } from 'src/components/auth-provider';
import {
ContentTabAssistant,
MIMIC_CONVERSATION_DELAY,
} from 'src/components/content-tab-assistant';
import { LOCAL_STORAGE_CHAT_MESSAGES_KEY, CLEAR_HISTORY_REMINDER_TIME } from 'src/constants';
import { useAuth } from 'src/hooks/use-auth';
import { useGetWpVersion } from 'src/hooks/use-get-wp-version';
import { useOffline } from 'src/hooks/use-offline';
import { ThemeDetailsProvider } from 'src/hooks/use-theme-details';
Expand All @@ -28,6 +29,7 @@ store.replaceReducer( testReducer );
vi.mock( 'src/hooks/use-offline' );
vi.mock( 'src/lib/get-ipc-api' );
vi.mock( 'src/hooks/use-get-wp-version' );
vi.mock( 'src/hooks/use-auth' );

vi.mock( 'src/lib/app-globals', () => ( {
getAppGlobals: () => ( {
Expand Down Expand Up @@ -129,14 +131,13 @@ describe( 'ContentTabAssistant', () => {
logout,
...auth,
};
vi.mocked( useAuth ).mockReturnValue( authContextValue );

return (
<Provider store={ store }>
<AuthContext.Provider value={ authContextValue }>
<ThemeDetailsProvider>
<ContentTabAssistant selectedSite={ selectedSite } />
</ThemeDetailsProvider>
</AuthContext.Provider>
<ThemeDetailsProvider>
<ContentTabAssistant selectedSite={ selectedSite } />
</ThemeDetailsProvider>
</Provider>
);
};
Expand Down
Loading
Loading