/** * UpdateChecker - React component for automatic update checking * * Wraps the application and automatically detects new versions, * showing a modal window with update progress */ const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => { const [updateState, setUpdateState] = React.useState({ hasUpdate: false, isUpdating: false, progress: 0, currentVersion: null, newVersion: null, showModal: false }); const updateManagerRef = React.useRef(null); // Initialize UpdateManager React.useEffect(() => { // Check that UpdateManager is available if (typeof window === 'undefined' || !window.UpdateManager) { console.error('❌ UpdateManager not found. Make sure updateManager.js is loaded.'); return; } // Create UpdateManager instance updateManagerRef.current = new window.UpdateManager({ versionUrl: '/meta.json', checkInterval: 60000, // 1 minute checkOnLoad: true, debug: debug, onUpdateAvailable: (updateInfo) => { setUpdateState(prev => ({ ...prev, hasUpdate: true, currentVersion: updateInfo.currentVersion, newVersion: updateInfo.newVersion, showModal: true })); // Call external callback if available if (onUpdateAvailable) { onUpdateAvailable(updateInfo); } }, onError: (error) => { if (debug) { console.warn('Update check error (non-critical):', error); } } }); // Cleanup on unmount return () => { if (updateManagerRef.current) { updateManagerRef.current.destroy(); } }; }, [onUpdateAvailable, debug]); // Force update handler const handleForceUpdate = async () => { if (!updateManagerRef.current || updateState.isUpdating) { return; } setUpdateState(prev => ({ ...prev, isUpdating: true, progress: 0 })); try { // Simulate update progress const progressSteps = [ { progress: 10, message: 'Saving data...' }, { progress: 30, message: 'Clearing Service Worker caches...' }, { progress: 50, message: 'Unregistering Service Workers...' }, { progress: 70, message: 'Clearing browser cache...' }, { progress: 90, message: 'Updating version...' }, { progress: 100, message: 'Reloading application...' } ]; for (const step of progressSteps) { await new Promise(resolve => setTimeout(resolve, 300)); setUpdateState(prev => ({ ...prev, progress: step.progress })); } // Start force update await updateManagerRef.current.forceUpdate(); } catch (error) { console.error('❌ Update failed:', error); setUpdateState(prev => ({ ...prev, isUpdating: false, progress: 0 })); // Show error to user alert('Update error. Please refresh the page manually (Ctrl+F5 or Cmd+Shift+R)'); } }; // Close modal (not recommended, but leaving the option) const handleCloseModal = () => { // Warn user if (window.confirm('New version available. Update is recommended for security and stability. Continue without update?')) { setUpdateState(prev => ({ ...prev, showModal: false })); } }; // Format version for display const formatVersion = (version) => { if (!version) return 'N/A'; // If version is timestamp, format as date if (/^\d+$/.test(version)) { const date = new Date(parseInt(version)); return date.toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } return version; }; const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace"; const SANS = "'Manrope', system-ui, -apple-system, sans-serif"; // Update modal — translated from the Claude Design component // (Update Notification.dc.html). Styling is inline so it tracks the design. return React.createElement(React.Fragment, null, [ // Main application content children, updateState.showModal && React.createElement('div', { key: 'update-modal', style: { position: 'fixed', inset: 0, zIndex: 9999, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px', background: 'rgba(8,8,10,0.55)', backdropFilter: 'blur(3px)', WebkitBackdropFilter: 'blur(3px)', animation: 'unFade .3s ease', fontFamily: SANS } }, [ React.createElement('style', { key: 'kf', dangerouslySetInnerHTML: { __html: '@keyframes unPop{from{opacity:0;transform:scale(.96) translateY(10px)}to{opacity:1;transform:scale(1) translateY(0)}}' + '@keyframes unFade{from{opacity:0}to{opacity:1}}' + '@keyframes unSpin{to{transform:rotate(360deg)}}' } }), React.createElement('div', { key: 'card', style: { position: 'relative', width: '440px', maxWidth: 'calc(100vw - 48px)', borderRadius: '22px', background: '#121214', border: '1px solid rgba(255,255,255,0.08)', padding: '36px 32px 28px', textAlign: 'center', boxShadow: '0 30px 70px rgba(0,0,0,0.6)', animation: 'unPop .32s cubic-bezier(.2,.7,.3,1)' } }, [ // spinning update icon React.createElement('div', { key: 'icon', style: { display: 'inline-flex', width: '64px', height: '64px', borderRadius: '50%', alignItems: 'center', justifyContent: 'center', background: 'rgba(240,137,42,0.12)', border: '1px solid rgba(240,137,42,0.3)', marginBottom: '20px' } }, React.createElement('svg', { width: 28, height: 28, viewBox: '0 0 24 24', fill: 'none', stroke: '#f0892a', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', style: { animation: 'unSpin 6s linear infinite' }, dangerouslySetInnerHTML: { __html: '' } })), React.createElement('h2', { key: 'title', style: { margin: '0 0 9px', fontSize: '26px', fontWeight: 800, letterSpacing: '-0.7px', color: '#f4f4f6' } }, 'Update available'), React.createElement('p', { key: 'sub', style: { margin: '0 0 24px', fontSize: '14.5px', lineHeight: 1.55, color: '#9a9aa2' } }, 'A newer version of SecureBit has been detected.'), // version comparison React.createElement('div', { key: 'vbox', style: { borderRadius: '14px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,0.06)', padding: '16px 18px', marginBottom: '24px', textAlign: 'left' } }, [ React.createElement('div', { key: 'cur', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '14px', padding: '5px 0' } }, [ React.createElement('span', { key: 'l', style: { fontSize: '13.5px', fontWeight: 500, color: '#8a8a92' } }, 'Current version'), React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 500, color: '#9a9aa2', whiteSpace: 'nowrap' } }, formatVersion(updateState.currentVersion)) ]), React.createElement('div', { key: 'sep', style: { height: '1px', background: 'rgba(255,255,255,0.05)', margin: '4px 0' } }), React.createElement('div', { key: 'new', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '14px', padding: '5px 0' } }, [ React.createElement('span', { key: 'l', style: { display: 'inline-flex', alignItems: 'center', gap: '8px', fontSize: '13.5px', fontWeight: 600, color: '#e8e8eb' } }, [ React.createElement('span', { key: 'd', style: { width: '6px', height: '6px', borderRadius: '50%', background: '#f0892a' } }), 'New version' ]), React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 700, color: '#f0892a', whiteSpace: 'nowrap' } }, formatVersion(updateState.newVersion)) ]) ]), // progress while updating, otherwise the action buttons updateState.isUpdating ? React.createElement('div', { key: 'progress' }, [ React.createElement('div', { key: 'bar', style: { width: '100%', height: '8px', borderRadius: '99px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,0.06)', overflow: 'hidden', marginBottom: '10px' } }, React.createElement('div', { key: 'fill', style: { height: '100%', width: `${updateState.progress}%`, background: 'linear-gradient(90deg,#3ecf8e,#f0892a)', transition: 'width .3s ease' } })), React.createElement('p', { key: 't', style: { margin: 0, fontFamily: MONO, fontSize: '12px', color: '#8a8a92' } }, `Updating… ${updateState.progress}%`) ]) : React.createElement('div', { key: 'actions', style: { display: 'flex', alignItems: 'center', gap: '12px' } }, [ React.createElement('button', { key: 'update', onClick: handleForceUpdate, style: { flex: 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '10px', padding: '15px 20px', borderRadius: '13px', border: 'none', background: '#f0892a', color: '#1a0f04', fontFamily: 'inherit', fontSize: '15.5px', fontWeight: 700, letterSpacing: '-0.2px', cursor: 'pointer', boxShadow: '0 8px 24px rgba(240,137,42,0.28)', transition: 'all .2s cubic-bezier(.2,.7,.3,1)' }, onMouseEnter: (e) => { e.currentTarget.style.background = '#ff9637'; e.currentTarget.style.transform = 'translateY(-2px)'; }, onMouseLeave: (e) => { e.currentTarget.style.background = '#f0892a'; e.currentTarget.style.transform = 'none'; } }, [ React.createElement('svg', { key: 'i', width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.1, strokeLinecap: 'round', strokeLinejoin: 'round', dangerouslySetInnerHTML: { __html: '' } }), 'Update now' ]), React.createElement('button', { key: 'later', onClick: handleCloseModal, title: 'Later', style: { flex: 'none', width: '50px', height: '50px', borderRadius: '13px', display: 'grid', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.025)', color: '#9a9aa2', cursor: 'pointer', transition: 'all .18s cubic-bezier(.2,.7,.3,1)' }, onMouseEnter: (e) => { e.currentTarget.style.color = '#e5727a'; e.currentTarget.style.borderColor = 'rgba(229,114,122,0.4)'; }, onMouseLeave: (e) => { e.currentTarget.style.color = '#9a9aa2'; e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)'; } }, React.createElement('svg', { width: 17, height: 17, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.1, strokeLinecap: 'round', strokeLinejoin: 'round', dangerouslySetInnerHTML: { __html: '' } })) ]) ]) ]) ]); }; // Export for use if (typeof module !== 'undefined' && module.exports) { module.exports = UpdateChecker; } else { window.UpdateChecker = UpdateChecker; }