ALwrity + Wordpress + Wix + GSC integration
This commit is contained in:
178
frontend/src/hooks/usePersonaPolling.ts
Normal file
178
frontend/src/hooks/usePersonaPolling.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { apiClient } from '../api/client';
|
||||
|
||||
export interface PersonaTaskStatus {
|
||||
task_id: string;
|
||||
status: string; // 'pending', 'running', 'completed', 'failed'
|
||||
progress: number; // 0-100
|
||||
current_step: string;
|
||||
progress_messages: Array<{
|
||||
timestamp: string;
|
||||
message: string;
|
||||
progress?: number;
|
||||
}>;
|
||||
result?: any;
|
||||
error?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface UsePersonaPollingOptions {
|
||||
interval?: number; // Polling interval in milliseconds
|
||||
maxAttempts?: number; // Maximum number of polling attempts
|
||||
onProgress?: (message: string, progress: number) => void; // Callback for progress updates
|
||||
onComplete?: (result: any) => void; // Callback when task completes
|
||||
onError?: (error: string) => void; // Callback when task fails
|
||||
}
|
||||
|
||||
export interface UsePersonaPollingReturn {
|
||||
isPolling: boolean;
|
||||
currentStatus: string;
|
||||
progress: number;
|
||||
currentStep: string;
|
||||
progressMessages: Array<{ timestamp: string; message: string; progress?: number }>;
|
||||
result: any;
|
||||
error: string | null;
|
||||
startPolling: (taskId: string) => void;
|
||||
stopPolling: () => void;
|
||||
}
|
||||
|
||||
export function usePersonaPolling(options: UsePersonaPollingOptions = {}): UsePersonaPollingReturn {
|
||||
const {
|
||||
interval = 2000, // 2 seconds default
|
||||
maxAttempts = 0, // No timeout - poll until backend says done
|
||||
onProgress,
|
||||
onComplete,
|
||||
onError
|
||||
} = options;
|
||||
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const [currentStatus, setCurrentStatus] = useState<string>('idle');
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
const [currentStep, setCurrentStep] = useState<string>('');
|
||||
const [progressMessages, setProgressMessages] = useState<Array<{ timestamp: string; message: string; progress?: number }>>([]);
|
||||
const [result, setResult] = useState<any>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Debug state changes
|
||||
useEffect(() => {
|
||||
console.log('Persona polling state changed:', {
|
||||
isPolling,
|
||||
currentStatus,
|
||||
progress,
|
||||
currentStep,
|
||||
progressCount: progressMessages.length
|
||||
});
|
||||
}, [isPolling, currentStatus, progress, currentStep, progressMessages.length]);
|
||||
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const attemptsRef = useRef(0);
|
||||
const currentTaskIdRef = useRef<string | null>(null);
|
||||
|
||||
const stopPolling = useCallback(() => {
|
||||
console.log('stopPersonaPolling called');
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
console.log('Setting isPolling to false');
|
||||
setIsPolling(false);
|
||||
attemptsRef.current = 0;
|
||||
currentTaskIdRef.current = null;
|
||||
}, []);
|
||||
|
||||
const startPolling = useCallback((taskId: string) => {
|
||||
console.log('startPersonaPolling called with taskId:', taskId);
|
||||
if (isPolling) {
|
||||
console.log('Already polling, stopping first');
|
||||
stopPolling();
|
||||
}
|
||||
|
||||
currentTaskIdRef.current = taskId;
|
||||
console.log('Setting isPolling to true');
|
||||
setIsPolling(true);
|
||||
setCurrentStatus('pending');
|
||||
setProgress(0);
|
||||
setCurrentStep('Initializing...');
|
||||
setProgressMessages([]);
|
||||
setResult(null);
|
||||
setError(null);
|
||||
attemptsRef.current = 0;
|
||||
|
||||
const poll = async () => {
|
||||
if (!currentTaskIdRef.current) {
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(`/api/onboarding/step4/persona-task/${currentTaskIdRef.current}`);
|
||||
const status: PersonaTaskStatus = response.data;
|
||||
|
||||
console.log('Persona polling status update:', status);
|
||||
setCurrentStatus(status.status);
|
||||
setProgress(status.progress);
|
||||
setCurrentStep(status.current_step);
|
||||
|
||||
// Update progress messages
|
||||
if (status.progress_messages && status.progress_messages.length > 0) {
|
||||
console.log('Progress messages received:', status.progress_messages);
|
||||
setProgressMessages(status.progress_messages);
|
||||
|
||||
// Call onProgress with the latest message
|
||||
const latestMessage = status.progress_messages[status.progress_messages.length - 1];
|
||||
console.log('Latest progress message:', latestMessage.message);
|
||||
onProgress?.(latestMessage.message, status.progress);
|
||||
}
|
||||
|
||||
if (status.status === 'completed') {
|
||||
setResult(status.result);
|
||||
onComplete?.(status.result);
|
||||
stopPolling();
|
||||
} else if (status.status === 'failed') {
|
||||
setError(status.error || 'Persona generation failed');
|
||||
onError?.(status.error || 'Persona generation failed');
|
||||
stopPolling();
|
||||
}
|
||||
|
||||
attemptsRef.current++;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||
console.error('Persona polling error:', errorMessage);
|
||||
|
||||
// Only stop polling for actual task failures (404, task not found)
|
||||
// For network errors, timeouts, etc., continue polling
|
||||
if (errorMessage.includes('404') || errorMessage.includes('Task not found')) {
|
||||
setError('Task not found - it may have expired or been cleaned up');
|
||||
onError?.('Task not found - it may have expired or been cleaned up');
|
||||
stopPolling();
|
||||
}
|
||||
// For other errors (timeouts, network issues), continue polling
|
||||
// The backend will eventually complete or fail, and we'll catch it
|
||||
}
|
||||
};
|
||||
|
||||
// Start polling immediately, then at intervals
|
||||
poll();
|
||||
intervalRef.current = setInterval(poll, interval);
|
||||
}, [isPolling, interval, onProgress, onComplete, onError, stopPolling]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
stopPolling();
|
||||
};
|
||||
}, [stopPolling]);
|
||||
|
||||
return {
|
||||
isPolling,
|
||||
currentStatus,
|
||||
progress,
|
||||
currentStep,
|
||||
progressMessages,
|
||||
result,
|
||||
error,
|
||||
startPolling,
|
||||
stopPolling
|
||||
};
|
||||
}
|
||||
75
frontend/src/hooks/useScramble.ts
Normal file
75
frontend/src/hooks/useScramble.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
|
||||
const CHARS = '!<>-_\\/[]{}—=+*^?#@%&|~';
|
||||
|
||||
interface ScrambleHookProps {
|
||||
text: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export const useScramble = ({ text, duration = 750 }: ScrambleHookProps) => {
|
||||
const [displayText, setDisplayText] = useState(text);
|
||||
|
||||
const timerRef = useRef<number | null>(null);
|
||||
const startTimeRef = useRef<number | null>(null);
|
||||
const isScramblingRef = useRef(false);
|
||||
|
||||
const getRandomChar = useCallback(() => {
|
||||
return CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||
}, []);
|
||||
|
||||
const update = useCallback(() => {
|
||||
if (startTimeRef.current === null || !isScramblingRef.current) return;
|
||||
|
||||
const elapsed = Date.now() - startTimeRef.current;
|
||||
const progress = elapsed / duration;
|
||||
|
||||
if (progress >= 1) {
|
||||
setDisplayText(text);
|
||||
isScramblingRef.current = false;
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
return;
|
||||
}
|
||||
|
||||
const newText = text.split('').map((char, index) => {
|
||||
if (char === ' ') return ' ';
|
||||
// Logic from original script: scramble all for 70% of duration, then reveal
|
||||
if (progress > 0.7) {
|
||||
const revealPoint = ((progress - 0.7) / 0.3) * text.length;
|
||||
return index < revealPoint ? text[index] : getRandomChar();
|
||||
}
|
||||
return getRandomChar();
|
||||
}).join('');
|
||||
|
||||
setDisplayText(newText);
|
||||
timerRef.current = window.setTimeout(update, 50);
|
||||
|
||||
}, [duration, text, getRandomChar]);
|
||||
|
||||
const start = useCallback(() => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
isScramblingRef.current = true;
|
||||
startTimeRef.current = Date.now();
|
||||
update();
|
||||
}, [update]);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
isScramblingRef.current = false;
|
||||
startTimeRef.current = null;
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
setDisplayText(text);
|
||||
}, [text]);
|
||||
|
||||
// When the source text changes, stop any animation and reset to the new text.
|
||||
useEffect(() => {
|
||||
stop();
|
||||
}, [text, stop]);
|
||||
|
||||
return { displayText, start, stop };
|
||||
};
|
||||
128
frontend/src/hooks/useWixConnection.ts
Normal file
128
frontend/src/hooks/useWixConnection.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Wix Connection Hook
|
||||
* Manages Wix connection state and operations
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useAuth } from '@clerk/clerk-react';
|
||||
import { wixAPI, WixStatus } from '../api/wix';
|
||||
|
||||
export const useWixConnection = () => {
|
||||
const { getToken } = useAuth();
|
||||
const [status, setStatus] = useState<WixStatus>({
|
||||
connected: false,
|
||||
sites: [],
|
||||
total_sites: 0
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Set up auth token getter for Wix API
|
||||
useEffect(() => {
|
||||
wixAPI.setAuthTokenGetter(async () => {
|
||||
try {
|
||||
const template = process.env.REACT_APP_CLERK_JWT_TEMPLATE;
|
||||
if (template) {
|
||||
// @ts-ignore Clerk types allow options object
|
||||
return await getToken({ template });
|
||||
}
|
||||
return await getToken();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}, [getToken]);
|
||||
|
||||
const checkStatus = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Check sessionStorage for Wix tokens (like WixTestPage does)
|
||||
const connectedFlag = sessionStorage.getItem('wix_connected') === 'true';
|
||||
const tokensRaw = sessionStorage.getItem('wix_tokens');
|
||||
|
||||
if (connectedFlag && tokensRaw) {
|
||||
const tokens = JSON.parse(tokensRaw);
|
||||
|
||||
// Try to get actual site information from Wix API
|
||||
try {
|
||||
const { createClient, OAuthStrategy } = await import('@wix/sdk');
|
||||
const wixClient = createClient({
|
||||
auth: OAuthStrategy({ clientId: '75d88e36-1c76-4009-b769-15f4654556df' })
|
||||
});
|
||||
wixClient.auth.setTokens(tokens);
|
||||
|
||||
// Get member info to extract site URL
|
||||
const memberInfo = await wixClient.auth.getMemberInfo();
|
||||
console.log('Wix member info:', memberInfo);
|
||||
|
||||
// Try to extract site URL from member info or use a default
|
||||
let siteUrl = 'Connected Wix Site';
|
||||
if (memberInfo?.member?.email) {
|
||||
// Extract domain from email or use email as identifier
|
||||
const email = memberInfo.member.email;
|
||||
const domain = email.split('@')[1];
|
||||
siteUrl = `https://${domain}`;
|
||||
}
|
||||
|
||||
setStatus({
|
||||
connected: true,
|
||||
sites: [{
|
||||
id: 'wix-site-1',
|
||||
blog_url: siteUrl,
|
||||
blog_id: 'wix-blog',
|
||||
created_at: new Date().toISOString(),
|
||||
scope: 'BLOG.CREATE-DRAFT,BLOG.PUBLISH,MEDIA.MANAGE'
|
||||
}],
|
||||
total_sites: 1
|
||||
});
|
||||
} catch (apiError) {
|
||||
console.log('Wix API error, using fallback:', apiError);
|
||||
// Fallback if API call fails
|
||||
setStatus({
|
||||
connected: true,
|
||||
sites: [{
|
||||
id: 'wix-site-1',
|
||||
blog_url: 'Connected Wix Site',
|
||||
blog_id: 'wix-blog',
|
||||
created_at: new Date().toISOString(),
|
||||
scope: 'BLOG.CREATE-DRAFT,BLOG.PUBLISH,MEDIA.MANAGE'
|
||||
}],
|
||||
total_sites: 1
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Wix status checked: connected via sessionStorage');
|
||||
} else {
|
||||
setStatus({
|
||||
connected: false,
|
||||
sites: [],
|
||||
total_sites: 0,
|
||||
error: 'No Wix connection found'
|
||||
});
|
||||
console.log('Wix status checked: not connected');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking Wix status:', error);
|
||||
setStatus({
|
||||
connected: false,
|
||||
sites: [],
|
||||
total_sites: 0,
|
||||
error: 'Error checking connection status'
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Check status on mount
|
||||
useEffect(() => {
|
||||
checkStatus();
|
||||
}, [checkStatus]);
|
||||
|
||||
return {
|
||||
connected: status.connected,
|
||||
sites: status.sites,
|
||||
totalSites: status.total_sites,
|
||||
isLoading,
|
||||
checkStatus
|
||||
};
|
||||
};
|
||||
249
frontend/src/hooks/useWordPressConnection.ts
Normal file
249
frontend/src/hooks/useWordPressConnection.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* WordPress Connection Hook
|
||||
* Manages WordPress site connections and publishing state.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { wordpressAPI, WordPressSite, WordPressStatusResponse } from '../api/wordpress';
|
||||
import { useAuth } from '@clerk/clerk-react';
|
||||
|
||||
export interface UseWordPressConnectionReturn {
|
||||
// Connection state
|
||||
connected: boolean;
|
||||
sites: WordPressSite[];
|
||||
totalSites: number;
|
||||
isLoading: boolean;
|
||||
|
||||
// Connection actions
|
||||
addSite: (siteData: {
|
||||
site_url: string;
|
||||
site_name: string;
|
||||
username: string;
|
||||
app_password: string;
|
||||
}) => Promise<boolean>;
|
||||
disconnectSite: (siteId: number) => Promise<boolean>;
|
||||
testConnection: (siteData: {
|
||||
site_url: string;
|
||||
site_name: string;
|
||||
username: string;
|
||||
app_password: string;
|
||||
}) => Promise<boolean>;
|
||||
|
||||
// Publishing actions
|
||||
publishContent: (publishData: {
|
||||
site_id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
excerpt?: string;
|
||||
featured_image_path?: string;
|
||||
categories?: string[];
|
||||
tags?: string[];
|
||||
status?: 'draft' | 'publish' | 'private';
|
||||
meta_description?: string;
|
||||
}) => Promise<{ success: boolean; post_id?: number; error?: string }>;
|
||||
|
||||
// Utility functions
|
||||
validateSiteUrl: (url: string) => boolean;
|
||||
formatSiteUrl: (url: string) => string;
|
||||
refreshStatus: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useWordPressConnection = (): UseWordPressConnectionReturn => {
|
||||
const { getToken } = useAuth();
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [sites, setSites] = useState<WordPressSite[]>([]);
|
||||
const [totalSites, setTotalSites] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Set up authentication
|
||||
useEffect(() => {
|
||||
const setupAuth = async () => {
|
||||
try {
|
||||
wordpressAPI.setAuthTokenGetter(async () => {
|
||||
try {
|
||||
return await getToken();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error setting up WordPress API auth:', error);
|
||||
}
|
||||
};
|
||||
|
||||
setupAuth();
|
||||
}, [getToken]);
|
||||
|
||||
// Check connection status on mount
|
||||
useEffect(() => {
|
||||
checkStatus();
|
||||
}, []);
|
||||
|
||||
const checkStatus = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const status: WordPressStatusResponse = await wordpressAPI.getStatus();
|
||||
|
||||
setConnected(status.connected);
|
||||
setSites(status.sites || []);
|
||||
setTotalSites(status.total_sites);
|
||||
|
||||
console.log('WordPress status checked:', status);
|
||||
} catch (error) {
|
||||
console.error('Error checking WordPress status:', error);
|
||||
setConnected(false);
|
||||
setSites([]);
|
||||
setTotalSites(0);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addSite = async (siteData: {
|
||||
site_url: string;
|
||||
site_name: string;
|
||||
username: string;
|
||||
app_password: string;
|
||||
}): Promise<boolean> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Format the site URL
|
||||
const formattedUrl = wordpressAPI.formatSiteUrl(siteData.site_url);
|
||||
|
||||
const site = await wordpressAPI.addSite({
|
||||
...siteData,
|
||||
site_url: formattedUrl
|
||||
});
|
||||
|
||||
// Update local state
|
||||
setSites(prev => [site, ...prev]);
|
||||
setTotalSites(prev => prev + 1);
|
||||
setConnected(true);
|
||||
|
||||
console.log('WordPress site added successfully:', site);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error adding WordPress site:', error);
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const disconnectSite = async (siteId: number): Promise<boolean> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const result = await wordpressAPI.disconnectSite(siteId);
|
||||
|
||||
if (result.success) {
|
||||
// Update local state
|
||||
setSites(prev => prev.filter(site => site.id !== siteId));
|
||||
setTotalSites(prev => Math.max(0, prev - 1));
|
||||
|
||||
// Check if we still have any connected sites
|
||||
const remainingSites = sites.filter(site => site.id !== siteId);
|
||||
setConnected(remainingSites.length > 0);
|
||||
|
||||
console.log('WordPress site disconnected successfully');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error disconnecting WordPress site:', error);
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const testConnection = async (siteData: {
|
||||
site_url: string;
|
||||
site_name: string;
|
||||
username: string;
|
||||
app_password: string;
|
||||
}): Promise<boolean> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Format the site URL
|
||||
const formattedUrl = wordpressAPI.formatSiteUrl(siteData.site_url);
|
||||
|
||||
const success = await wordpressAPI.testConnection({
|
||||
...siteData,
|
||||
site_url: formattedUrl
|
||||
});
|
||||
|
||||
console.log('WordPress connection test result:', success);
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error('Error testing WordPress connection:', error);
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const publishContent = async (publishData: {
|
||||
site_id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
excerpt?: string;
|
||||
featured_image_path?: string;
|
||||
categories?: string[];
|
||||
tags?: string[];
|
||||
status?: 'draft' | 'publish' | 'private';
|
||||
meta_description?: string;
|
||||
}): Promise<{ success: boolean; post_id?: number; error?: string }> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const result = await wordpressAPI.publishContent(publishData);
|
||||
|
||||
console.log('WordPress content published:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error publishing WordPress content:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const validateSiteUrl = useCallback((url: string): boolean => {
|
||||
return wordpressAPI.validateSiteUrl(url);
|
||||
}, []);
|
||||
|
||||
const formatSiteUrl = useCallback((url: string): string => {
|
||||
return wordpressAPI.formatSiteUrl(url);
|
||||
}, []);
|
||||
|
||||
const refreshStatus = useCallback(async (): Promise<void> => {
|
||||
await checkStatus();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// Connection state
|
||||
connected,
|
||||
sites,
|
||||
totalSites,
|
||||
isLoading,
|
||||
|
||||
// Connection actions
|
||||
addSite,
|
||||
disconnectSite,
|
||||
testConnection,
|
||||
|
||||
// Publishing actions
|
||||
publishContent,
|
||||
|
||||
// Utility functions
|
||||
validateSiteUrl,
|
||||
formatSiteUrl,
|
||||
refreshStatus
|
||||
};
|
||||
};
|
||||
178
frontend/src/hooks/useWordPressOAuth.ts
Normal file
178
frontend/src/hooks/useWordPressOAuth.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* WordPress OAuth Connection Hook
|
||||
* Manages WordPress.com OAuth2 authentication flow.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { wordpressOAuthAPI, WordPressOAuthStatus, WordPressOAuthSite } from '../api/wordpressOAuth';
|
||||
import { useAuth } from '@clerk/clerk-react';
|
||||
|
||||
export interface UseWordPressOAuthReturn {
|
||||
// Connection state
|
||||
connected: boolean;
|
||||
sites: WordPressOAuthSite[];
|
||||
totalSites: number;
|
||||
isLoading: boolean;
|
||||
|
||||
// OAuth actions
|
||||
startOAuthFlow: () => Promise<void>;
|
||||
disconnectSite: (tokenId: number) => Promise<boolean>;
|
||||
refreshStatus: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useWordPressOAuth = (): UseWordPressOAuthReturn => {
|
||||
const { getToken } = useAuth();
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [sites, setSites] = useState<WordPressOAuthSite[]>([]);
|
||||
const [totalSites, setTotalSites] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Set up authentication
|
||||
useEffect(() => {
|
||||
const setupAuth = async () => {
|
||||
try {
|
||||
wordpressOAuthAPI.setAuthTokenGetter(async () => {
|
||||
try {
|
||||
return await getToken();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error setting up WordPress OAuth API auth:', error);
|
||||
}
|
||||
};
|
||||
|
||||
setupAuth();
|
||||
}, [getToken]);
|
||||
|
||||
// Check connection status on mount
|
||||
useEffect(() => {
|
||||
checkStatus();
|
||||
}, []);
|
||||
|
||||
const checkStatus = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const status: WordPressOAuthStatus = await wordpressOAuthAPI.getStatus();
|
||||
|
||||
setConnected(status.connected);
|
||||
setSites(status.sites || []);
|
||||
setTotalSites(status.total_sites);
|
||||
|
||||
console.log('WordPress OAuth status checked:', status);
|
||||
} catch (error) {
|
||||
console.error('Error checking WordPress OAuth status:', error);
|
||||
setConnected(false);
|
||||
setSites([]);
|
||||
setTotalSites(0);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startOAuthFlow = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const authData = await wordpressOAuthAPI.getAuthUrl();
|
||||
|
||||
if (authData && authData.auth_url) {
|
||||
// Open OAuth popup window
|
||||
const popup = window.open(
|
||||
authData.auth_url,
|
||||
'wordpress-oauth',
|
||||
'width=600,height=700,scrollbars=yes,resizable=yes'
|
||||
);
|
||||
|
||||
if (!popup) {
|
||||
throw new Error('Popup blocked. Please allow popups for this site.');
|
||||
}
|
||||
|
||||
// Listen for popup completion and messages
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
// Accept messages only from the popup we opened and from trusted origins
|
||||
const trustedOrigins = [window.location.origin, 'https://littery-sonny-unscrutinisingly.ngrok-free.dev'];
|
||||
if (event.source !== popup) return;
|
||||
if (!trustedOrigins.includes(event.origin)) return;
|
||||
|
||||
if (event.data.type === 'WPCOM_OAUTH_SUCCESS') {
|
||||
popup.close();
|
||||
clearInterval(checkClosed);
|
||||
// Refresh status after OAuth completion
|
||||
setTimeout(() => {
|
||||
checkStatus();
|
||||
}, 1000);
|
||||
} else if (event.data.type === 'WPCOM_OAUTH_ERROR') {
|
||||
popup.close();
|
||||
clearInterval(checkClosed);
|
||||
console.error('WordPress OAuth error:', event.data.error);
|
||||
// Refresh status to show disconnected state
|
||||
setTimeout(() => {
|
||||
checkStatus();
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
const checkClosed = setInterval(() => {
|
||||
if (popup.closed) {
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('message', messageHandler);
|
||||
// Refresh status after OAuth completion
|
||||
setTimeout(() => {
|
||||
checkStatus();
|
||||
}, 1000);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
console.log('WordPress OAuth flow started');
|
||||
} else {
|
||||
throw new Error('Failed to get WordPress OAuth URL');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting WordPress OAuth flow:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const disconnectSite = async (tokenId: number): Promise<boolean> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const result = await wordpressOAuthAPI.disconnectSite(tokenId);
|
||||
|
||||
if (result.success) {
|
||||
// Refresh status after disconnection
|
||||
await checkStatus();
|
||||
console.log('WordPress site disconnected successfully');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error disconnecting WordPress site:', error);
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshStatus = useCallback(async (): Promise<void> => {
|
||||
await checkStatus();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// Connection state
|
||||
connected,
|
||||
sites,
|
||||
totalSites,
|
||||
isLoading,
|
||||
|
||||
// OAuth actions
|
||||
startOAuthFlow,
|
||||
disconnectSite,
|
||||
refreshStatus
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user