Google - v1.0.0
React Component
GoogleConnectionManager is a React component that consumes the JSON status endpoint and renders the same connect / disconnect / reauthorize UI as the Livewire and Vue surfaces.
Getting the source
The component ships as source under resources/js/react/ — no prebuilt npm package. Two ways to use it:
Publish to your app's tree:
php artisan vendor:publish --tag=google-js
This copies both React and Vue components to resources/js/vendor/google/.
Or point your bundler at the package directly. In vite.config.js:
export default defineConfig({
resolve: {
alias: {
'@artisanpack-ui/google/react': path.resolve(
__dirname,
'vendor/artisanpack-ui/google/resources/js/react'
),
},
},
});
Then import:
import { GoogleConnectionManager } from '@artisanpack-ui/google/react';
Requires React 18+ and a bundler that can compile .tsx (Vite, Webpack + ts-loader, etc.).
Basic usage
import { GoogleConnectionManager } from '../vendor/google/react';
export function IntegrationsPage() {
return (
<GoogleConnectionManager
onDisconnected={ () => console.log( 'disconnected' ) }
/>
);
}
The component fetches /google/auth/status on mount, renders the appropriate UI, and re-fetches after a successful disconnect. Errors from the status fetch are caught and rendered with a Retry button.
Props
All optional:
| Prop | Type | Default | Purpose |
|---|---|---|---|
statusUrl |
string |
/google/auth/status |
Override the status endpoint if you've changed google.routes.prefix. |
csrfToken |
string | null |
Read from <meta name="csrf-token"> |
Sent as X-CSRF-TOKEN on the disconnect POST. |
labels |
Partial<GoogleConnectionLabels> |
English defaults from shared/labels.ts |
Localized labels. See below. |
onDisconnected |
() => void |
— | Fires after a successful disconnect. |
onStatusChanged |
(status: GoogleConnectionStatus) => void |
— | Fires whenever the status payload refreshes. |
className |
string |
— | Appended to the root element's className. |
Labels
The React component can't call Laravel's __(), so localization goes through the labels prop. Wire it from Blade:
<div id="google-connection-manager" data-labels="@json([
'loading' => __( 'Loading Google connection…' ),
'connectButton' => __( 'Connect Google' ),
'disconnectButton' => __( 'Disconnect' ),
'reauthorizeButton' => __( 'Reauthorize' ),
'connectedAs' => __( 'Connected as' ),
'noAccountConnected' => __( 'No Google account connected.' ),
'errorFallback' => __( 'Unable to load Google connection status.' ),
'retry' => __( 'Retry' ),
'disconnectingButton' => __( 'Disconnecting…' ),
])"></div>
Then read the JSON attribute when mounting:
const el = document.getElementById( 'google-connection-manager' );
const labels = JSON.parse( el?.dataset.labels ?? '{}' );
createRoot( el ).render(
<GoogleConnectionManager
labels={{
...labels,
// Pluralization callbacks can't cross the JSON boundary — wire in code:
needsReauthorize: ( count ) => trans_choice( 'reauth.count', count ),
grantedScopesTitle: ( count ) => `${labels.grantedScopesTitle}: ${count}`,
requiredScopesTitle: ( count ) => `${labels.requiredScopesTitle}: ${count}`,
}}
/>
);
The full label shape:
export type GoogleConnectionLabels = {
loading: string;
retry: string;
errorFallback: string;
connectedAs: string;
googleAccountFallback: string;
noAccountConnected: string;
connectButton: string;
reauthorizeButton: string;
disconnectButton: string;
disconnectingButton: string;
needsReauthorize: ( count: number ) => string;
grantedScopesTitle: ( count: number ) => string;
requiredScopesTitle: ( count: number ) => string;
};
Every key is optional in the prop; omitted keys fall back to the English defaults exported as defaultGoogleConnectionLabels.
The shared API client
Both the React and Vue components import from resources/js/shared/api.ts. If you want to build your own UI on top of the same client:
import {
fetchGoogleConnectionStatus,
disconnectGoogleConnection,
GoogleConnectionApiError,
type GoogleConnectionStatus,
} from '../vendor/google/shared/api';
const status = await fetchGoogleConnectionStatus();
if ( ! status.connected ) {
window.location.href = status.urls.connect;
}
await disconnectGoogleConnection( {
disconnectUrl: status.urls.disconnect,
csrfToken: '...', // read from <meta name="csrf-token"> when omitted
} );
Both helpers throw GoogleConnectionApiError on non-OK responses, with the HTTP status attached.
Ref-stashed callbacks
The component stashes onStatusChanged and onDisconnected in refs to avoid re-fetching on every parent render. A parent that hoists status into its own state via onStatusChanged would otherwise trigger a fresh fetch on every render — and if that fetch updated the parent, an infinite loop.
This is why onStatusChanged and onDisconnected aren't part of the effect's dependency array — the ref pattern keeps the latest callback callable without listing it as a dep.
Testing
Mock the fetch calls:
import { render, screen } from '@testing-library/react';
import { GoogleConnectionManager } from './ConnectionManager';
beforeEach( () => {
globalThis.fetch = jest.fn().mockResolvedValue( {
ok: true,
json: async () => ( {
connected: false,
email: null,
grantedScopes: [],
requiredScopes: [ 'openid' ],
missingScopes: [ 'openid' ],
needsReauthorize: false,
disconnectReason: null,
urls: { connect: '/c', reauthorize: '/r', disconnect: '/d' },
} ),
} );
} );
test( 'renders the connect button when disconnected', async () => {
render( <GoogleConnectionManager /> );
expect( await screen.findByRole( 'link', { name: /connect google/i } ) ).toBeInTheDocument();
} );