diff --git a/src/hooks/sync-sites/use-listen-deep-link-connection.ts b/src/hooks/sync-sites/use-listen-deep-link-connection.ts
index f329986bfe..8e50b375c6 100644
--- a/src/hooks/sync-sites/use-listen-deep-link-connection.ts
+++ b/src/hooks/sync-sites/use-listen-deep-link-connection.ts
@@ -2,6 +2,7 @@ import { useAuth } from 'src/hooks/use-auth';
import { useContentTabs } from 'src/hooks/use-content-tabs';
import { useIpcListener } from 'src/hooks/use-ipc-listener';
import { useSiteDetails } from 'src/hooks/use-site-details';
+import { SyncSite } from 'src/modules/sync/types';
import { useAppDispatch } from 'src/stores';
import {
connectedSitesActions,
@@ -28,25 +29,65 @@ export function useListenDeepLinkConnection() {
useIpcListener(
'sync-connect-site',
- async ( _event, { remoteSiteId, studioSiteId, autoOpenPush } ) => {
- // Fetch latest sites from network before checking
- const result = await refetchWpComSites();
- const latestSites = result.data ?? [];
- const newConnectedSite = latestSites.find( ( site ) => site.id === remoteSiteId );
- if ( newConnectedSite ) {
- if ( selectedSite?.id && selectedSite.id !== studioSiteId ) {
- // Select studio site that started the sync
- setSelectedSiteId( studioSiteId );
- }
- await connectSite( { site: newConnectedSite, localSiteId: studioSiteId } );
- if ( selectedTab !== 'sync' ) {
- // Switch to sync tab
- setSelectedTab( 'sync' );
- }
- // Only auto-open push dialog if explicitly requested (e.g., from "Publish site" button)
- if ( autoOpenPush ) {
- dispatch( connectedSitesActions.setSelectedRemoteSiteId( remoteSiteId ) );
- }
+ async (
+ _event,
+ {
+ remoteSiteId,
+ studioSiteId,
+ autoOpenPush,
+ siteName,
+ siteUrl,
+ }: {
+ remoteSiteId: number;
+ studioSiteId: string;
+ autoOpenPush?: boolean;
+ siteName?: string;
+ siteUrl?: string;
+ }
+ ) => {
+ // Create minimal site object optimistically to connect immediately
+ // Use siteName and siteUrl from deeplink if available, otherwise use placeholders
+ const minimalSite: SyncSite = {
+ id: remoteSiteId,
+ localSiteId: studioSiteId,
+ name: siteName || 'Loading site...', // Use provided name or placeholder
+ url: siteUrl || '', // Use provided URL or empty string
+ isStaging: false, // Default assumption
+ isPressable: false, // Default assumption
+ environmentType: null, // Will be fetched
+ syncSupport: 'already-connected', // Safe default for new connections
+ lastPullTimestamp: null, // New site, no history
+ lastPushTimestamp: null, // New site, no history
+ };
+
+ // Switch to the site that initiated the connection if needed
+ if ( selectedSite?.id && selectedSite.id !== studioSiteId ) {
+ setSelectedSiteId( studioSiteId );
+ }
+
+ // Switch to sync tab
+ if ( selectedTab !== 'sync' ) {
+ setSelectedTab( 'sync' );
+ }
+
+ // Connect optimistically (async, don't block modal opening)
+ const connectPromise = connectSite( { site: minimalSite, localSiteId: studioSiteId } );
+
+ // Only auto-open push dialog if explicitly requested (e.g., from "Publish site" button)
+ // Open modal immediately with minimal data
+ if ( autoOpenPush ) {
+ dispatch( connectedSitesActions.setSelectedRemoteSiteId( remoteSiteId ) );
+ }
+
+ // Fetch full site data in background (don't await - parallel operation)
+ const refetchPromise = refetchWpComSites();
+
+ // Wait for both operations to complete for error handling
+ try {
+ await Promise.all( [ connectPromise, refetchPromise ] );
+ } catch ( error ) {
+ console.error( 'Error during site connection:', error );
+ // Connection or refetch failed - the UI will handle the error state via mutation status
}
}
);
diff --git a/src/ipc-utils.ts b/src/ipc-utils.ts
index e5e8de413b..aaea53100a 100644
--- a/src/ipc-utils.ts
+++ b/src/ipc-utils.ts
@@ -47,7 +47,15 @@ export interface IpcEvents {
'snapshot-key-value': [ { operationId: crypto.UUID; data: SnapshotKeyValueEventData } ];
'snapshot-success': [ { operationId: crypto.UUID } ];
'show-whats-new': [ void ];
- 'sync-connect-site': [ { remoteSiteId: number; studioSiteId: string; autoOpenPush?: boolean } ];
+ 'sync-connect-site': [
+ {
+ remoteSiteId: number;
+ studioSiteId: string;
+ autoOpenPush?: boolean;
+ siteName?: string;
+ siteUrl?: string;
+ },
+ ];
'test-render-failure': [ void ];
'theme-details-changed': [ { id: string; details: StartedSiteDetails[ 'themeDetails' ] } ];
'theme-details-updating': [ { id: string } ];
diff --git a/src/lib/deeplink/handlers/sync-connect-site.ts b/src/lib/deeplink/handlers/sync-connect-site.ts
index 0bf504786b..fd010277e1 100644
--- a/src/lib/deeplink/handlers/sync-connect-site.ts
+++ b/src/lib/deeplink/handlers/sync-connect-site.ts
@@ -10,12 +10,16 @@ export async function handleSyncConnectSiteDeeplink( urlObject: URL ): Promise<
const remoteSiteId = parseInt( searchParams.get( 'remoteSiteId' ) ?? '' );
const studioSiteId = searchParams.get( 'studioSiteId' );
const autoOpenPush = searchParams.get( 'autoOpenPush' ) === 'true';
+ const siteName = searchParams.get( 'siteName' ) ?? undefined;
+ const siteUrl = searchParams.get( 'siteUrl' ) ?? undefined;
if ( remoteSiteId && studioSiteId ) {
void sendIpcEventToRenderer( 'sync-connect-site', {
remoteSiteId,
studioSiteId,
autoOpenPush,
+ siteName,
+ siteUrl,
} );
}
}
diff --git a/src/modules/sync/components/sync-dialog.tsx b/src/modules/sync/components/sync-dialog.tsx
index 72392f3450..8741aecb52 100644
--- a/src/modules/sync/components/sync-dialog.tsx
+++ b/src/modules/sync/components/sync-dialog.tsx
@@ -36,6 +36,7 @@ type SyncDialogProps = {
onPush: ( syncData: TreeNode[] ) => void;
onPull: ( syncData: TreeNode[] ) => void;
onRequestClose: () => void;
+ isConnectionReady?: boolean;
};
const useDynamicTreeState = (
@@ -135,6 +136,7 @@ export function SyncDialog( {
onPush,
onPull,
onRequestClose,
+ isConnectionReady = true,
}: SyncDialogProps ) {
const locale = useI18nLocale();
const { __ } = useI18n();
@@ -445,10 +447,11 @@ export function SyncDialog( {
isSubmitDisabled ||
isLoadingRewindId ||
isPushSelectionOverLimit ||
- isSizeCheckLoading
+ isSizeCheckLoading ||
+ ! isConnectionReady
}
>
- { syncTexts.submit }
+ { ! isConnectionReady ? __( 'Connecting...' ) : syncTexts.submit }
diff --git a/src/modules/sync/index.tsx b/src/modules/sync/index.tsx
index 8a88c56824..5a03174a0d 100644
--- a/src/modules/sync/index.tsx
+++ b/src/modules/sync/index.tsx
@@ -1,6 +1,6 @@
import { check, Icon } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
-import { PropsWithChildren, useEffect, useState } from 'react';
+import { PropsWithChildren, useCallback, useEffect, useState } from 'react';
import { ArrowIcon } from 'src/components/arrow-icon';
import Button from 'src/components/button';
import offlineIcon from 'src/components/offline-icon';
@@ -136,7 +136,7 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
localSiteId: selectedSite.id,
userId: user?.id,
} );
- const [ connectSite ] = useConnectSiteMutation();
+ const [ connectSite, { isLoading: isConnecting } ] = useConnectSiteMutation();
const [ disconnectSite ] = useDisconnectSiteMutation();
const { pushSite, pullSite } = useSyncSites();
@@ -146,8 +146,19 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
userId: user?.id,
} );
+ // Merge connectedSites with syncSites to get the most up-to-date data
+ // This ensures the Sync tab shows current data even before reconciliation updates storage
+ const mergedConnectedSites = connectedSites.map( ( connectedSite ) => {
+ const syncSite = syncSites.find( ( site ) => site.id === connectedSite.id );
+ // If we have data from the API (syncSites), use it; otherwise use storage data
+ return syncSite || connectedSite;
+ } );
+
const [ selectedRemoteSite, setSelectedRemoteSite ] = useState< SyncSite | null >( null );
+ // Check if connection is ready using RTK Query's built-in loading state
+ const isConnectionReady = ! isConnecting;
+
// Auto-select remote site when set via Redux (e.g., from deep link connection)
useEffect( () => {
if ( selectedRemoteSiteId ) {
@@ -160,21 +171,46 @@ export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails }
}
}, [ selectedRemoteSiteId, syncSites, dispatch ] );
+ // Update selectedRemoteSite when syncSites updates with more complete data
+ // This ensures the modal shows updated site info when background refetch completes
+ useEffect( () => {
+ if ( selectedRemoteSite ) {
+ const updatedSite = syncSites.find( ( site ) => site.id === selectedRemoteSite.id );
+ if ( updatedSite && updatedSite !== selectedRemoteSite ) {
+ // Update with the more complete site data from refetch
+ setSelectedRemoteSite( updatedSite );
+ }
+ }
+ }, [ syncSites, selectedRemoteSite ] );
+
+ const handleConnect = useCallback(
+ async ( newConnectedSite: SyncSite ) => {
+ // Check if already connected (use connectedSites from storage as source of truth)
+ const isAlreadyConnected = connectedSites.some( ( site ) => site.id === newConnectedSite.id );
+ if ( isAlreadyConnected ) {
+ // Site is already connected, no need to reconnect
+ return;
+ }
+
+ // Note: Connection status check is handled by the disabled button state
+ // If connection is pending, the button will be disabled
+
+ try {
+ await connectSite( { site: newConnectedSite, localSiteId: selectedSite.id } );
+ } catch ( error ) {
+ getIpcApi().showErrorMessageBox( {
+ title: __( 'Failed to connect to site' ),
+ message: __( 'Please try again.' ),
+ } );
+ }
+ },
+ [ connectedSites, connectSite, selectedSite.id, __ ]
+ );
+
if ( ! isAuthenticated ) {
return