Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d4a35d3
Add Auth Slice
Mar 12, 2026
598c654
Solve linter
Mar 12, 2026
f5d9d57
Fix tests
Mar 12, 2026
81cd242
Clean up duplicate callbacks
Mar 12, 2026
149709e
Fix unncessary export
Mar 12, 2026
d342280
Fix linter
Mar 12, 2026
724fa49
Merge branch 'trunk' of github.com:Automattic/studio into fix/refacto…
Mar 19, 2026
4e8d6d5
Adjust implementation
Mar 19, 2026
b6bc9fb
Initialize locale
Mar 19, 2026
fe87aad
Futher adjustments
Mar 19, 2026
319cc7d
Adjust store
Mar 19, 2026
28cc95a
Fix regression
Mar 19, 2026
1566f4a
Merge trunk into fix/refactor-auth-provider-in-redux-slice
Mar 27, 2026
6819301
Merge branch 'trunk' of github.com:Automattic/studio into fix/refacto…
Apr 8, 2026
649648f
Fix any of type error
Apr 8, 2026
ae8e8d2
Add tests
Apr 8, 2026
317fbcf
Remove test that is too complex and is not needed
Apr 8, 2026
0fdfacd
Final fix
Apr 8, 2026
706793e
Merge branch 'trunk' of github.com:Automattic/studio into fix/refacto…
Apr 8, 2026
6ed3623
Merge branch 'trunk' of github.com:Automattic/studio into fix/refacto…
Apr 24, 2026
547baf8
Fix error
Apr 24, 2026
eca9dc4
Remove test and use locator directly
Apr 24, 2026
bc04dec
ADJUST EVENT handler
Apr 24, 2026
b511fb8
Adjust locale
Apr 24, 2026
3a0aa72
Merge branch 'trunk' of github.com:Automattic/studio into fix/refacto…
Apr 27, 2026
24c66db
Use better practice for auth trunk
Apr 27, 2026
b170b06
Remove use-auth
Apr 27, 2026
10f81f2
Add guards for unit tests
Apr 27, 2026
928ffbe
Solve for linter
Apr 27, 2026
95401c0
Another test fix
Apr 27, 2026
379f5d8
Test fix
Apr 27, 2026
f2c6f44
tESTS FIX
Apr 27, 2026
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Should be done ✅

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 ] );
Comment thread
katinthehatsite marked this conversation as resolved.
Outdated

useIpcListener( 'auth-updated', ( _event, payload ) => {
Comment thread
katinthehatsite marked this conversation as resolved.
Outdated
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