diff --git a/apps/studio/src/components/auth-provider.tsx b/apps/studio/src/components/auth-provider.tsx deleted file mode 100644 index 8afb587e36..0000000000 --- a/apps/studio/src/components/auth-provider.tsx +++ /dev/null @@ -1,223 +0,0 @@ -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 { 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 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 - 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, - authenticate: () => { - // Placeholder for authenticate logic. Just to avoid lint error - }, - logout: () => Promise.resolve(), -} ); - -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 locale = useI18nLocale(); - const { __ } = useI18n(); - const isOffline = useOffline(); - - 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 ); - } - }, [] ); - - 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 = __( - 'It looks like you denied the authorization request. To proceed, please click "Approve"' - ); - } - - void getIpcApi().showErrorMessageBox( { title, message } ); - 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 || '', - } ); - } ); - - 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 ); - - 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, - isAuthenticated, - authenticate, - logout, - user, - } ), - [ client, isAuthenticated, authenticate, logout, user ] - ); - - return { children }; -}; - -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; diff --git a/apps/studio/src/components/root.tsx b/apps/studio/src/components/root.tsx index d23e6b0959..e3e6bced51 100644 --- a/apps/studio/src/components/root.tsx +++ b/apps/studio/src/components/root.tsx @@ -5,7 +5,6 @@ import { I18nProvider } from '@wordpress/react-i18n'; import { useEffect } from 'react'; import { Provider as ReduxProvider } from 'react-redux'; import App from 'src/components/app'; -import AuthProvider from 'src/components/auth-provider'; import CrashTester from 'src/components/crash-tester'; import ErrorBoundary from 'src/components/error-boundary'; import { WordPressStyles } from 'src/components/wordpress-styles'; @@ -35,21 +34,19 @@ const Root = () => { - - - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/apps/studio/src/components/tests/content-tab-assistant.test.tsx b/apps/studio/src/components/tests/content-tab-assistant.test.tsx index 6d5251cbfa..4d6b3a2f5f 100644 --- a/apps/studio/src/components/tests/content-tab-assistant.test.tsx +++ b/apps/studio/src/components/tests/content-tab-assistant.test.tsx @@ -7,12 +7,12 @@ 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 { ContentTabAssistant, MIMIC_CONVERSATION_DELAY, } from 'src/components/content-tab-assistant'; import { LOCAL_STORAGE_CHAT_MESSAGES_KEY, CLEAR_HISTORY_REMINDER_TIME } from 'src/constants'; +import { AuthContextType, 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'; @@ -28,6 +28,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: () => ( { @@ -129,14 +130,13 @@ describe( 'ContentTabAssistant', () => { logout, ...auth, }; + vi.mocked( useAuth ).mockReturnValue( authContextValue ); return ( - - - - - + + + ); }; diff --git a/apps/studio/src/hooks/use-auth.ts b/apps/studio/src/hooks/use-auth.ts index 6ed814d1d5..686543bb7f 100644 --- a/apps/studio/src/hooks/use-auth.ts +++ b/apps/studio/src/hooks/use-auth.ts @@ -1,12 +1,37 @@ -import { useContext } from 'react'; -import { AuthContext, type AuthContextType } from 'src/components/auth-provider'; +import { useCallback } from 'react'; +import { useOffline } from 'src/hooks/use-offline'; +import { getIpcApi } from 'src/lib/get-ipc-api'; +import { useAppDispatch, useRootSelector } from 'src/stores'; +import { authLogout, selectIsAuthenticated, selectUser } from 'src/stores/auth-slice'; +import { getWpcomClient } from 'src/stores/wpcom-client'; +import type { AuthUser } from 'src/stores/auth-slice'; +import type { WPCOM } from 'wpcom/types'; + +export interface AuthContextType { + client: WPCOM | undefined; + isAuthenticated: boolean; + authenticate: () => void; + logout: () => Promise< void >; + user?: AuthUser; +} export const useAuth = (): AuthContextType => { - const context = useContext( AuthContext ); + const dispatch = useAppDispatch(); + const isOffline = useOffline(); + const isAuthenticated = useRootSelector( selectIsAuthenticated ); + const user = useRootSelector( selectUser ); + + const authenticate = useCallback( () => getIpcApi().authenticate(), [] ); - if ( ! context ) { - throw new Error( 'useAuth must be used within an AuthProvider' ); - } + const logout = useCallback( async () => { + await dispatch( authLogout( { isOffline } ) ); + }, [ dispatch, isOffline ] ); - return context; + return { + client: isAuthenticated ? getWpcomClient() : undefined, + isAuthenticated, + authenticate, + logout, + user, + }; }; diff --git a/apps/studio/src/modules/preview-site/components/create-preview-button.tsx b/apps/studio/src/modules/preview-site/components/create-preview-button.tsx index 9b8cf12ccd..3bdd3972f9 100644 --- a/apps/studio/src/modules/preview-site/components/create-preview-button.tsx +++ b/apps/studio/src/modules/preview-site/components/create-preview-button.tsx @@ -1,10 +1,10 @@ import { DEMO_SITE_SIZE_LIMIT_GB } from '@studio/common/constants'; import { __, sprintf } from '@wordpress/i18n'; import { useI18n } from '@wordpress/react-i18n'; -import { AuthContextType } from 'src/components/auth-provider'; import Button from 'src/components/button'; import offlineIcon from 'src/components/offline-icon'; import { Tooltip } from 'src/components/tooltip'; +import { AuthContextType } from 'src/hooks/use-auth'; import { useGetWpVersion } from 'src/hooks/use-get-wp-version'; import { useOffline } from 'src/hooks/use-offline'; import { useSiteSize } from 'src/hooks/use-site-size'; diff --git a/apps/studio/src/stores/auth-slice.ts b/apps/studio/src/stores/auth-slice.ts new file mode 100644 index 0000000000..7af9e14180 --- /dev/null +++ b/apps/studio/src/stores/auth-slice.ts @@ -0,0 +1,214 @@ +import { createAsyncThunk, createSlice, isAnyOf } from '@reduxjs/toolkit'; +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 { __ } from '@wordpress/i18n'; +import { getIpcApi } from 'src/lib/get-ipc-api'; +import { isInvalidTokenError } from 'src/lib/is-invalid-oauth-token-error'; +import { store, RootState } from 'src/stores'; +import { getWpcomClient, setWpcomClient } from 'src/stores/wpcom-client'; +import type { StoredToken } from 'src/lib/oauth'; +import type { WPCOM } from 'wpcom/types'; + +interface WpcomParams extends Record< string, unknown > { + query?: string; + apiNamespace?: string; +} + +export type AuthUser = { id: number; email: string; displayName: string }; + +type AuthState = { + isAuthenticated: boolean; + user: AuthUser | null; +}; + +const initialState: AuthState = { + isAuthenticated: false, + user: null, +}; + +function createWpcomClient( token?: string, locale?: string, onInvalidToken?: () => void ): WPCOM { + 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; + }; + + 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 && isInvalidTokenError( err ) && onInvalidToken && ! isAuthErrorDialogOpen ) { + onInvalidToken(); + } + if ( typeof callback === 'function' ) { + callback( err, response, headers ); + } + }; + + return wpcomXhrRequest( modifiedParams, wrappedCallback ); + }; + + return wpcomFactory( token, wrappedRequestHandler ); +} + +const createTypedAsyncThunk = createAsyncThunk.withTypes< { state: RootState } >(); + +let isAuthErrorDialogOpen = false; + +export const handleInvalidToken = createTypedAsyncThunk( 'auth/handleInvalidToken', async () => { + if ( isAuthErrorDialogOpen ) { + return; + } + isAuthErrorDialogOpen = true; + try { + void getIpcApi().logRendererMessage( 'info', 'Detected invalid token. Logging out.' ); + await getIpcApi().clearAuthenticationToken(); + setWpcomClient( undefined ); + await getIpcApi().showMessageBox( { + type: 'error', + message: __( 'Session Expired' ), + detail: __( 'Your session has expired. Please log in again.' ), + } ); + } catch ( err ) { + console.error( 'Failed to handle invalid token:', err ); + Sentry.captureException( err ); + } finally { + isAuthErrorDialogOpen = false; + } +} ); + +export const initializeAuth = createTypedAsyncThunk( + 'auth/initialize', + async ( { locale }: { locale?: string } ) => { + try { + const token = await getIpcApi().getAuthenticationToken(); + + if ( ! token ) { + return null; + } + + const client = createWpcomClient( token.accessToken, locale, () => + store.dispatch( handleInvalidToken() ) + ); + setWpcomClient( client ); + + return { + id: token.id, + email: token.email, + displayName: token.displayName || '', + }; + } catch ( err ) { + console.error( err ); + Sentry.captureException( err ); + return null; + } + } +); + +export const authTokenReceived = createTypedAsyncThunk( + 'auth/tokenReceived', + async ( { token, locale }: { token: StoredToken; locale?: string } ) => { + const client = createWpcomClient( token.accessToken, locale, () => + store.dispatch( handleInvalidToken() ) + ); + setWpcomClient( client ); + + return { + id: token.id, + email: token.email, + displayName: token.displayName || '', + }; + } +); + +export const authLogout = createTypedAsyncThunk( + 'auth/logout', + async ( { isOffline }: { isOffline: boolean } ) => { + const client = getWpcomClient(); + + if ( ! isOffline && client ) { + try { + await client.req.del( { + apiNamespace: 'wpcom/v2', + path: '/studio-app/token', + 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(); + setWpcomClient( undefined ); + } catch ( err ) { + console.error( err ); + Sentry.captureException( err ); + } + } +); + +const authSlice = createSlice( { + name: 'auth', + initialState, + reducers: {}, + extraReducers: ( builder ) => { + builder + .addCase( initializeAuth.fulfilled, ( state, action ) => { + state.isAuthenticated = !! action.payload; + state.user = action.payload ?? null; + } ) + .addCase( authTokenReceived.fulfilled, ( state, action ) => { + state.isAuthenticated = true; + state.user = action.payload; + } ) + .addMatcher( isAnyOf( handleInvalidToken.fulfilled, authLogout.fulfilled ), ( state ) => { + state.isAuthenticated = false; + state.user = null; + } ); + }, +} ); + +export const selectIsAuthenticated = ( state: RootState ) => state.auth.isAuthenticated; +export const selectUser = ( state: RootState ) => state.auth.user ?? undefined; + +export function initializeAuthIpcListeners() { + window.ipcListener.subscribe( 'auth-updated', ( _event, payload ) => { + if ( 'error' in payload ) { + let title: string = __( 'Authentication error' ); + let message: string = __( 'Please try again.' ); + + if ( payload.error instanceof Error && payload.error.message.includes( 'access_denied' ) ) { + title = __( 'Authorization denied' ); + message = __( + 'It looks like you denied the authorization request. To proceed, please click "Approve"' + ); + } + + void getIpcApi().showErrorMessageBox( { title, message } ); + return; + } + + const locale = store.getState().i18n.locale; + void store.dispatch( authTokenReceived( { token: payload.token, locale } ) ); + } ); +} + +export default authSlice.reducer; diff --git a/apps/studio/src/stores/index.ts b/apps/studio/src/stores/index.ts index eed7e4a25b..e91c913107 100644 --- a/apps/studio/src/stores/index.ts +++ b/apps/studio/src/stores/index.ts @@ -14,10 +14,11 @@ import { } from 'src/hooks/use-sync-states-progress-info'; import { getIpcApi } from 'src/lib/get-ipc-api'; import { appVersionApi } from 'src/stores/app-version-api'; +import authReducer, { initializeAuth, initializeAuthIpcListeners } from 'src/stores/auth-slice'; import { betaFeaturesReducer, loadBetaFeatures } from 'src/stores/beta-features-slice'; import { certificateTrustApi } from 'src/stores/certificate-trust-api'; import { reducer as chatReducer } from 'src/stores/chat-slice'; -import i18nReducer from 'src/stores/i18n-slice'; +import i18nReducer, { initializeUserLocale, saveUserLocale } from 'src/stores/i18n-slice'; import { installedAppsApi } from 'src/stores/installed-apps-api'; import onboardingReducer from 'src/stores/onboarding-slice'; import { @@ -34,11 +35,13 @@ import { } from 'src/stores/sync/sync-operations-slice'; import { wpcomSitesApi } from 'src/stores/sync/wpcom-sites'; import uiReducer from 'src/stores/ui-slice'; -import { getWpcomClient, wpcomApi, wpcomPublicApi } from 'src/stores/wpcom-api'; +import { wpcomApi, wpcomPublicApi } from 'src/stores/wpcom-api'; +import { getWpcomClient } from 'src/stores/wpcom-client'; import { wordpressVersionsApi } from './wordpress-versions-api'; import type { SupportedLocale } from '@studio/common/lib/locale'; export type RootState = { + auth: ReturnType< typeof authReducer >; appVersionApi: ReturnType< typeof appVersionApi.reducer >; betaFeatures: ReturnType< typeof betaFeaturesReducer >; chat: ReturnType< typeof chatReducer >; @@ -290,6 +293,15 @@ async function startPullPoller( selectedSiteId: string, remoteSiteId: number ) { } } +// Initialize auth once locale is loaded, and re-initialize when locale changes +startAppListening( { + matcher: isAnyOf( initializeUserLocale.fulfilled, saveUserLocale.fulfilled ), + effect( action, listenerApi ) { + const { locale } = listenerApi.getState().i18n; + void listenerApi.dispatch( initializeAuth( { locale } ) ); + }, +} ); + // Poll push progress when state enters a pollable status startAppListening( { actionCreator: syncOperationsActions.updatePushState, @@ -321,6 +333,7 @@ startAppListening( { } ); export const rootReducer = combineReducers( { + auth: authReducer, appVersionApi: appVersionApi.reducer, betaFeatures: betaFeaturesReducer, chat: chatReducer, @@ -360,6 +373,7 @@ setupListeners( store.dispatch ); // Initialize beta features on store initialization, but skip in test environment if ( process.env.NODE_ENV !== 'test' ) { + initializeAuthIpcListeners(); void store.dispatch( loadBetaFeatures() ); } diff --git a/apps/studio/src/stores/sync/wpcom-sites.ts b/apps/studio/src/stores/sync/wpcom-sites.ts index 9d1f634b56..c7fd2f0048 100644 --- a/apps/studio/src/stores/sync/wpcom-sites.ts +++ b/apps/studio/src/stores/sync/wpcom-sites.ts @@ -5,7 +5,7 @@ import { getIpcApi } from 'src/lib/get-ipc-api'; import { reconcileConnectedSites } from 'src/modules/sync/lib/reconcile-connected-sites'; import { getSyncSupport, isPressableSite } from 'src/modules/sync/lib/sync-support'; import { withOfflineCheck } from 'src/stores/utils/with-offline-check'; -import { getWpcomClient } from 'src/stores/wpcom-api'; +import { getWpcomClient } from 'src/stores/wpcom-client'; import type { SyncSite, SyncSupport } from 'src/modules/sync/types'; // Schema for WordPress.com sites endpoint diff --git a/apps/studio/src/stores/wpcom-api.ts b/apps/studio/src/stores/wpcom-api.ts index 176f12f16f..cfb6f582ae 100644 --- a/apps/studio/src/stores/wpcom-api.ts +++ b/apps/studio/src/stores/wpcom-api.ts @@ -5,8 +5,8 @@ import wpcomFactory from '@studio/common/lib/wpcom-factory'; import wpcomXhrRequest from '@studio/common/lib/wpcom-xhr-request-factory'; import { z } from 'zod'; import { withOfflineCheck, withOfflineCheckMutation } from 'src/stores/utils/with-offline-check'; +import { getWpcomClient } from 'src/stores/wpcom-client'; import type { BaseQueryFn, FetchBaseQueryError } from '@reduxjs/toolkit/query'; -import type { WPCOM } from 'wpcom/types'; const welcomeMessageSchema = z.object( { messages: z.array( z.string() ), @@ -70,24 +70,15 @@ const blueprintSchema = z.object( { export type Blueprint = z.infer< typeof blueprintSchema >; -let wpcomClient: WPCOM | undefined; const publicWpcomClient = wpcomFactory( wpcomXhrRequest ); -export const setWpcomClient = ( client: WPCOM | undefined ) => { - wpcomClient = client; -}; - -export const getWpcomClient = (): WPCOM | undefined => { - return wpcomClient; -}; - const wpcomBaseQuery: BaseQueryFn< { path: string; apiNamespace?: string }, unknown, FetchBaseQueryError > = async ( args ) => { try { - const response = await wpcomClient!.req.get( args ); + const response = await getWpcomClient()!.req.get( args ); return { data: response }; } catch ( error ) { return { @@ -247,7 +238,7 @@ function withWpcomClientCheck< TResult, TArg >( return ( arg, options = {} ) => { return useQueryHook( arg, { ...options, - skip: ! wpcomClient || options?.skip, + skip: ! getWpcomClient() || options?.skip, } ); }; } @@ -258,7 +249,7 @@ function withWpcomClientCheckMutation< TResult, TArg >( return ( options = {} ) => { const [ trigger, result ] = useMutationHook( options ); const wrappedTrigger = ( ( ...args: Parameters< typeof trigger > ) => { - if ( ! wpcomClient ) { + if ( ! getWpcomClient() ) { return Promise.reject( new Error( 'Not authenticated' ) ) as ReturnType< typeof trigger >; } return trigger( ...args ); diff --git a/apps/studio/src/stores/wpcom-client.ts b/apps/studio/src/stores/wpcom-client.ts new file mode 100644 index 0000000000..9f8fe34b43 --- /dev/null +++ b/apps/studio/src/stores/wpcom-client.ts @@ -0,0 +1,11 @@ +import type { WPCOM } from 'wpcom/types'; + +let wpcomClient: WPCOM | undefined; + +export const setWpcomClient = ( client: WPCOM | undefined ) => { + wpcomClient = client; +}; + +export const getWpcomClient = (): WPCOM | undefined => { + return wpcomClient; +};