import { useAuth } from '@/hooks/use-auth'; import { usePage } from '@inertiajs/react'; import { useEffect, useRef, useState } from 'react'; interface Props { live_class: CourseLiveClass; watchHistory: WatchHistory; zoom_sdk_client_id?: string; zoom_sdk_client_secret?: string; } interface MeetingConfig { role: number; password: string; signature: string; meetingNumber: string; } // Zoom Client View SDK type declarations declare global { interface Window { ZoomMtg: { setZoomJSLib: (path: string, dir: string) => void; preLoadWasm: () => void; prepareWebSDK: () => void; checkSystemRequirements: () => any; i18n: { load: (lang: string) => void; onLoad: (callback: () => void) => void; }; init: (config: { leaveUrl: string; disableCORP?: boolean; success: () => void; error: (error: any) => void }) => void; join: (config: { meetingNumber: string; userName: string; signature: string; userEmail?: string; passWord?: string; success: (res: any) => void; error: (res: any) => void; }) => void; leave: () => void; inMeetingServiceListener: (event: string, callback: (data: any) => void) => void; getAttendeeslist: (config: any) => void; getCurrentUser: (config: any) => void; }; } } const ZoomLiveClass = ({ live_class, watchHistory, zoom_sdk_client_id }: Props) => { const { props } = usePage(); const { auth, translate } = props as any; const { frontend } = translate; const { isAdmin } = useAuth(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [sdkLoaded, setSdkLoaded] = useState(false); const initializationRef = useRef(false); const redirectUrl = isAdmin ? `/dashboard/courses` : `/play-course/${live_class.course.slug}/${watchHistory.id}/${watchHistory.current_watching_id}`; // Get meeting info const meetingInfo = (() => { if (!live_class.additional_info) return null; try { if (typeof live_class.additional_info === 'object') { return live_class.additional_info; } if (typeof live_class.additional_info === 'string') { return JSON.parse(live_class.additional_info); } return null; } catch (error) { // Error parsing meeting info return null; } })(); // Load Zoom Client View SDK scripts const loadZoomSDK = async () => { // Zoom SDK already loaded if (window.ZoomMtg) { setSdkLoaded(true); return Promise.resolve(); } // Loading Zoom Client View SDK scripts // Script loading order is important for Client View SDK const scripts = [ 'https://source.zoom.us/4.0.0/lib/vendor/react.min.js', 'https://source.zoom.us/4.0.0/lib/vendor/react-dom.min.js', 'https://source.zoom.us/4.0.0/lib/vendor/redux.min.js', 'https://source.zoom.us/4.0.0/lib/vendor/redux-thunk.min.js', 'https://source.zoom.us/4.0.0/zoom-meeting-4.0.0.min.js', // Client View SDK ]; for (const scriptSrc of scripts) { await new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = scriptSrc; script.async = false; // Load in order script.onload = () => { resolve(); }; script.onerror = () => { reject(new Error(`Failed to load script: ${scriptSrc}`)); }; document.head.appendChild(script); // Add timeout setTimeout(() => reject(new Error(`Script load timeout: ${scriptSrc}`)), 10000); }); } // All Zoom Client View SDK scripts loaded successfully setSdkLoaded(true); }; // Fetch signature and meeting config from backend const fetchMeetingConfig = async () => { try { setLoading(true); const response = await fetch(route('live-class.signature', live_class.id), { method: 'GET', headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', }, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data.error) { throw new Error(data.error); } return data; } catch (error) { setError(error instanceof Error ? error.message : frontend.failed_to_get_meeting_configuration); return null; } }; // Initialize Zoom Client View SDK const initializeZoomSDK = async (config: MeetingConfig) => { try { if (!window.ZoomMtg) { throw new Error(frontend.zoom_sdk_not_loaded); } // Check system requirements window.ZoomMtg.checkSystemRequirements(); // Clear the React root to avoid conflicts with Zoom's DOM manipulation const appElement = document.getElementById('app') || document.body; appElement.innerHTML = ''; // Prepare body for Zoom to take over document.body.style.margin = '0'; document.body.style.padding = '0'; document.body.style.overflow = 'hidden'; document.body.style.height = '100vh'; document.body.style.width = '100vw'; // Pre-load WASM and prepare SDK window.ZoomMtg.preLoadWasm(); window.ZoomMtg.prepareWebSDK(); // Load language (default to English) window.ZoomMtg.i18n.load('en-US'); // Initialize SDK when language is loaded window.ZoomMtg.i18n.onLoad(() => { window.ZoomMtg.init({ leaveUrl: window.location.origin + redirectUrl, // Redirect after leaving disableCORP: !window.crossOriginIsolated, // Required for security success: () => { joinMeeting(config); }, error: (error: any) => { setError(frontend.failed_to_initialize_meeting + ': ' + (error.message || error)); setLoading(false); }, }); }); // Set up meeting event listeners setupMeetingListeners(); } catch (error) { setError(error instanceof Error ? error.message : frontend.failed_to_initialize_meeting); setLoading(false); } }; // Join the meeting const joinMeeting = (config: MeetingConfig) => { window.ZoomMtg.join({ meetingNumber: config.meetingNumber, userName: auth.user.name, signature: config.signature, userEmail: auth.user.email || '', passWord: config.password, success: (res: any) => { // At this point, Zoom has taken over the page completely // Our React component should step aside setLoading(false); // Get current user info window.ZoomMtg.getCurrentUser({ success: (res: any) => { // Current user info }, }); // Get attendees list window.ZoomMtg.getAttendeeslist({}); }, error: (error: any) => { setError(frontend.failed_to_join_meeting + ': ' + (error.message || error)); setLoading(false); }, }); }; // Setup meeting event listeners const setupMeetingListeners = () => { // User join event window.ZoomMtg.inMeetingServiceListener('onUserJoin', (data: any) => { // User joined }); // User leave event window.ZoomMtg.inMeetingServiceListener('onUserLeave', (data: any) => { // User left }); // Waiting room event window.ZoomMtg.inMeetingServiceListener('onUserIsInWaitingRoom', (data: any) => { // User in waiting room }); // Meeting status change window.ZoomMtg.inMeetingServiceListener('onMeetingStatus', (data: any) => { // Meeting status changed }); }; // Cleanup function const cleanup = () => { if (window.ZoomMtg) { try { window.ZoomMtg.leave(); } catch (error) { // Error leaving meeting during cleanup } } }; // Main initialization useEffect useEffect(() => { if (initializationRef.current) return; initializationRef.current = true; const initializeMeeting = async () => { // Check if SDK credentials are available if (!zoom_sdk_client_id) { setError(frontend.zoom_sdk_not_configured); setLoading(false); return; } // Check if meeting info exists if (!meetingInfo) { setError(frontend.meeting_information_not_found); setLoading(false); return; } try { // Step 1: Load SDK scripts await loadZoomSDK(); // Step 2: Fetch meeting config const config = await fetchMeetingConfig(); if (!config) return; // Step 3: Initialize and join meeting await initializeZoomSDK(config); } catch (error) { setError(error instanceof Error ? error.message : frontend.failed_to_initialize_meeting); setLoading(false); } }; initializeMeeting(); // Cleanup on unmount return cleanup; }, []); // Render loading state if (loading) { return (

{live_class.class_topic}

{!sdkLoaded ? frontend.loading_zoom_sdk : frontend.joining_meeting}

); } // Render error state if (error) { return (

{frontend.unable_to_join_meeting}

{error}

{/* Fallback: Show direct Zoom link if available */} {meetingInfo?.join_url && (

{frontend.you_can_join_directly}

{frontend.open_in_zoom_app}
)}
); } return null; }; export default ZoomLiveClass;