@@ -5,19 +5,23 @@ import { setUser } from '@automattic/calypso-sentry';
55import { isSupportUserSession } from '@automattic/calypso-support-session' ;
66import { magnificentNonEnLocales } from '@automattic/i18n-utils' ;
77import {
8+ hashKey ,
89 useQuery ,
910 useQueryClient ,
1011 type QueryCacheNotifyEvent ,
1112 type MutationCacheNotifyEvent ,
1213} from '@tanstack/react-query' ;
1314import { createContext , useContext , useMemo , useEffect , useRef , useCallback } from 'react' ;
1415import { wpcomLink } from '../../utils/link' ;
16+ import { bumpStat } from '../analytics' ;
1517import { useAppContext } from '../context' ;
1618import { OAUTH_CALLBACK_PATH } from './oauth-callback' ;
1719import type { WPError } from '@automattic/api-core' ;
1820
1921export const AUTH_QUERY_KEY = [ 'auth' , 'user' ] ;
2022
23+ const BOOTSTRAP_ERROR_MESSAGE = 'Failed to bootstrap user object' ;
24+
2125function getOAuthAuthorizeUrl ( {
2226 state,
2327 next = '' ,
@@ -56,22 +60,37 @@ interface AuthContextType {
5660}
5761export const AuthContext = createContext < AuthContextType | undefined > ( undefined ) ;
5862
59- export async function initializeCurrentUser ( ) : Promise < User > {
63+ function shouldUseBootstrap ( ) : boolean {
6064 // In support user session the `currentUser` refers to the wrong person so we should request
6165 // the user object. Note we do not check `isSupportNextSession()` because in "next" support
6266 // sessions the server does bootstrap the correct `currentUser`.
63- const useBootstrap = ! isSupportUserSession ( ) && config . isEnabled ( 'wpcom-user-bootstrap' ) ;
67+ return ! isSupportUserSession ( ) && config . isEnabled ( 'wpcom-user-bootstrap' ) ;
68+ }
6469
65- if ( useBootstrap ) {
70+ export async function initializeCurrentUser ( ) : Promise < User > {
71+ if ( shouldUseBootstrap ( ) ) {
6672 if ( window . currentUser ) {
6773 return window . currentUser ;
6874 }
69- throw new Error ( 'Failed to bootstrap user object' ) ;
75+ throw new Error ( BOOTSTRAP_ERROR_MESSAGE ) ;
7076 }
7177
7278 return fetchUser ( ) ;
7379}
7480
81+ function getAuthErrorReason ( error : unknown ) : string {
82+ if ( error instanceof Error && error . message === BOOTSTRAP_ERROR_MESSAGE ) {
83+ return 'bootstrap' ;
84+ }
85+ if (
86+ isWpError ( error ) &&
87+ ( error . error === 'authorization_required' || error . statusCode === 401 )
88+ ) {
89+ return 'unauthorized' ;
90+ }
91+ return 'error' ;
92+ }
93+
7594/**
7695 * This component:
7796 * 1. Fetches and provides auth data via context
@@ -87,6 +106,7 @@ export function AuthProvider( { children }: { children: React.ReactNode } ) {
87106 data : user ,
88107 isLoading : userIsLoading ,
89108 isError : userIsError ,
109+ error : userError ,
90110 } = useQuery ( {
91111 queryKey : AUTH_QUERY_KEY ,
92112 queryFn : initializeCurrentUser ,
@@ -108,37 +128,42 @@ export function AuthProvider( { children }: { children: React.ReactNode } ) {
108128 } ;
109129 } , [ user ] ) ;
110130
111- const handleAuthError = useCallback ( ( ) => {
112- // Prevents repeated calls to redirect
113- if ( authErrorHandled . current ) {
114- return ;
115- }
131+ const handleAuthError = useCallback (
132+ ( reason : string ) => {
133+ // Prevents repeated calls to redirect
134+ if ( authErrorHandled . current ) {
135+ return ;
136+ }
116137
117- authErrorHandled . current = true ;
138+ authErrorHandled . current = true ;
118139
119- if ( config . isEnabled ( 'oauth' ) ) {
120- const state = crypto . randomUUID ( ) ;
121- sessionStorage . setItem ( 'wpcom_oauth_state' , state ) ;
140+ bumpStat ( 'dashboard-auth' , `bounce:${ reason } ` ) ;
122141
123- // Default to the signup screen rather than the login screen for certain routes.
124- const isNewUser =
125- supports . startStoreRoute === true && window . location . pathname === '/start-store' ;
142+ if ( config . isEnabled ( 'oauth' ) ) {
143+ const state = crypto . randomUUID ( ) ;
144+ sessionStorage . setItem ( 'wpcom_oauth_state' , state ) ;
126145
127- window . location . replace (
128- getOAuthAuthorizeUrl ( {
129- state,
130- isNewUser,
131- next : window . location . pathname + window . location . search ,
132- } )
133- ) ;
134- return ;
135- }
146+ // Default to the signup screen rather than the login screen for certain routes.
147+ const isNewUser =
148+ supports . startStoreRoute === true && window . location . pathname === '/start-store' ;
136149
137- const currentPath = window . location . href ;
138- const path = config ( 'wpcom_login_url' ) || wpcomLink ( '/log-in' ) ;
139- const loginUrl = `${ path } ?redirect_to=${ encodeURIComponent ( currentPath ) } ` ;
140- window . location . href = loginUrl ;
141- } , [ supports . startStoreRoute ] ) ;
150+ window . location . replace (
151+ getOAuthAuthorizeUrl ( {
152+ state,
153+ isNewUser,
154+ next : window . location . pathname + window . location . search ,
155+ } )
156+ ) ;
157+ return ;
158+ }
159+
160+ const currentPath = window . location . href ;
161+ const path = config ( 'wpcom_login_url' ) || wpcomLink ( '/log-in' ) ;
162+ const loginUrl = `${ path } ?redirect_to=${ encodeURIComponent ( currentPath ) } ` ;
163+ window . location . href = loginUrl ;
164+ } ,
165+ [ supports . startStoreRoute ]
166+ ) ;
142167
143168 // Subscribe to network errors and when errors occur due to being logged
144169 // out, redirect the user to the log in screen.
@@ -148,13 +173,18 @@ export function AuthProvider( { children }: { children: React.ReactNode } ) {
148173 } ;
149174
150175 const handleEvent = ( event : MutationCacheNotifyEvent | QueryCacheNotifyEvent ) => {
176+ // Errors fetching the user object itself are handled (and classified) below.
177+ if ( 'query' in event && event . query . queryHash === hashKey ( AUTH_QUERY_KEY ) ) {
178+ return ;
179+ }
180+
151181 if (
152182 event . type === 'updated' &&
153183 event . action . type === 'error' &&
154184 isWpError ( event . action . error ) &&
155185 isAuthError ( event . action . error )
156186 ) {
157- handleAuthError ( ) ;
187+ handleAuthError ( 'expired' ) ;
158188 }
159189 } ;
160190 const unsubMutationCache = queryClient . getMutationCache ( ) . subscribe ( handleEvent ) ;
@@ -165,17 +195,23 @@ export function AuthProvider( { children }: { children: React.ReactNode } ) {
165195 } ;
166196 } , [ queryClient , handleAuthError ] ) ;
167197
198+ const successStatBumped = useRef ( false ) ;
168199 useEffect ( ( ) => {
169200 if ( user ?. ID ) {
170201 setUser ( { id : user . ID . toString ( ) } ) ;
202+
203+ if ( ! successStatBumped . current ) {
204+ successStatBumped . current = true ;
205+ bumpStat ( 'dashboard-auth' , shouldUseBootstrap ( ) ? 'success:bootstrap' : 'success:fetch' ) ;
206+ }
171207 }
172208 } , [ user ] ) ;
173209
174210 // Handles _all_ errors fetching the user object, regardless of whether they are
175211 // `authorization_required` errors or not.
176212 if ( userIsError ) {
177213 if ( typeof window !== 'undefined' ) {
178- handleAuthError ( ) ;
214+ handleAuthError ( getAuthErrorReason ( userError ) ) ;
179215 }
180216 return null ;
181217 }
0 commit comments