NuvioStreaming/src/hooks/useUpdatePopup.ts
2025-09-21 18:17:43 +02:00

237 lines
7.9 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { Platform } from 'react-native';
import { toast, ToastPosition } from '@backpackapp-io/react-native-toast';
import UpdateService, { UpdateInfo } from '../services/updateService';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface UseUpdatePopupReturn {
showUpdatePopup: boolean;
updateInfo: UpdateInfo;
isInstalling: boolean;
checkForUpdates: () => Promise<void>;
handleUpdateNow: () => Promise<void>;
handleUpdateLater: () => void;
handleDismiss: () => void;
}
const UPDATE_POPUP_STORAGE_KEY = '@update_popup_dismissed';
const UPDATE_LATER_STORAGE_KEY = '@update_later_timestamp';
const UPDATE_LAST_CHECK_TS_KEY = '@update_last_check_ts';
const UPDATE_BADGE_KEY = '@update_badge_pending';
export const useUpdatePopup = (): UseUpdatePopupReturn => {
const [showUpdatePopup, setShowUpdatePopup] = useState(false);
const [updateInfo, setUpdateInfo] = useState<UpdateInfo>({ isAvailable: false });
const [isInstalling, setIsInstalling] = useState(false);
const [hasCheckedOnStartup, setHasCheckedOnStartup] = useState(false);
const [isAppReady, setIsAppReady] = useState(false);
const checkForUpdates = useCallback(async (forceCheck = false) => {
try {
// Skip update checks on Android to prevent OTA checks
if (Platform.OS === 'android') {
return;
}
// Check if user has dismissed the popup for this version
const dismissedVersion = await AsyncStorage.getItem(UPDATE_POPUP_STORAGE_KEY);
const currentVersion = updateInfo.manifest?.id;
if (dismissedVersion === currentVersion && !forceCheck) {
return; // User already dismissed this version
}
// Check if user chose "later" recently (within 6 hours)
const updateLaterTimestamp = await AsyncStorage.getItem(UPDATE_LATER_STORAGE_KEY);
if (updateLaterTimestamp && !forceCheck) {
const laterTime = parseInt(updateLaterTimestamp);
const now = Date.now();
const sixHours = 6 * 60 * 60 * 1000; // Reduced from 24 hours
if (now - laterTime < sixHours) {
return; // User chose "later" recently
}
}
const info = await UpdateService.checkForUpdates();
setUpdateInfo(info);
if (info.isAvailable) {
// Show popup (Android checks are handled earlier in the function)
setShowUpdatePopup(true);
}
} catch (error) {
if (__DEV__) console.error('Error checking for updates:', error);
// Don't show popup on error, just log it
}
}, [updateInfo.manifest?.id, isAppReady]);
const handleUpdateNow = useCallback(async () => {
try {
setIsInstalling(true);
setShowUpdatePopup(false);
const success = await UpdateService.downloadAndInstallUpdate();
if (success) {
// Update installed successfully - no restart alert needed
// The app will automatically reload with the new version
console.log('Update installed successfully');
} else {
toast('Unable to install the update. Please try again later or check your internet connection.', {
duration: 3000,
position: ToastPosition.TOP,
});
// Show popup again after failed installation
setShowUpdatePopup(true);
}
} catch (error) {
if (__DEV__) console.error('Error installing update:', error);
toast('An error occurred while installing the update. Please try again later.', {
duration: 3000,
position: ToastPosition.TOP,
});
// Show popup again after error
setShowUpdatePopup(true);
} finally {
setIsInstalling(false);
}
}, []);
const handleUpdateLater = useCallback(async () => {
try {
// Store timestamp when user chose "later"
await AsyncStorage.setItem(UPDATE_LATER_STORAGE_KEY, Date.now().toString());
setShowUpdatePopup(false);
} catch (error) {
if (__DEV__) console.error('Error storing update later preference:', error);
setShowUpdatePopup(false);
}
}, []);
const handleDismiss = useCallback(async () => {
try {
// Store the current version ID so we don't show popup again for this version
const currentVersion = updateInfo.manifest?.id;
if (currentVersion) {
await AsyncStorage.setItem(UPDATE_POPUP_STORAGE_KEY, currentVersion);
}
setShowUpdatePopup(false);
} catch (error) {
if (__DEV__) console.error('Error storing dismiss preference:', error);
setShowUpdatePopup(false);
}
}, [updateInfo.manifest?.id]);
// Handle startup update check results
useEffect(() => {
// Skip startup update check registration on Android
if (Platform.OS === 'android') {
return;
}
const handleStartupUpdateCheck = (updateInfo: UpdateInfo) => {
console.log('UpdatePopup: Received startup update check result', updateInfo);
setUpdateInfo(updateInfo);
setHasCheckedOnStartup(true);
if (updateInfo.isAvailable) {
if (Platform.OS === 'android') {
// Set badge and show a toast
(async () => {
try { await AsyncStorage.setItem(UPDATE_BADGE_KEY, 'true'); } catch {}
})();
try {
toast('Update available — go to Settings → App Updates', {
duration: 3000,
position: ToastPosition.TOP,
});
} catch {}
setShowUpdatePopup(false);
} else {
setShowUpdatePopup(true);
}
}
};
// Register callback for startup update check
UpdateService.onUpdateCheck(handleStartupUpdateCheck);
// Cleanup callback on unmount
return () => {
UpdateService.offUpdateCheck(handleStartupUpdateCheck);
};
}, []);
// Mark app as ready after a delay (Android safety)
useEffect(() => {
const timer = setTimeout(() => {
setIsAppReady(true);
}, Platform.OS === 'android' ? 3000 : 1000);
return () => clearTimeout(timer);
}, []);
// Show popup when app becomes ready on Android (if update is available)
useEffect(() => {
if (Platform.OS === 'android' && isAppReady && updateInfo.isAvailable && !showUpdatePopup) {
// Check if user hasn't dismissed this version
(async () => {
try {
const dismissedVersion = await AsyncStorage.getItem(UPDATE_POPUP_STORAGE_KEY);
const currentVersion = updateInfo.manifest?.id;
if (dismissedVersion !== currentVersion) {
setShowUpdatePopup(true);
}
} catch (error) {
// If we can't check, show the popup anyway
setShowUpdatePopup(true);
}
})();
}
}, [isAppReady, updateInfo.isAvailable, updateInfo.manifest?.id, showUpdatePopup]);
// Auto-check for updates when hook is first used (fallback if startup check fails)
useEffect(() => {
if (hasCheckedOnStartup) {
return; // Already checked on startup
}
// Skip auto-check on Android to prevent OTA checks
if (Platform.OS === 'android') {
return;
}
// Add a small delay to ensure the app is fully loaded
const timer = setTimeout(() => {
(async () => {
try {
const lastCheckTs = await AsyncStorage.getItem(UPDATE_LAST_CHECK_TS_KEY);
const last = lastCheckTs ? parseInt(lastCheckTs, 10) : 0;
const now = Date.now();
const sixHours = 6 * 60 * 60 * 1000; // Reduced from 24 hours
if (now - last < sixHours) {
return; // Throttle: only auto-check once per 6h
}
await checkForUpdates();
await AsyncStorage.setItem(UPDATE_LAST_CHECK_TS_KEY, String(now));
} catch {
// ignore
}
})();
}, 3000); // Increased delay to 3 seconds to give startup check time
return () => clearTimeout(timer);
}, [checkForUpdates, hasCheckedOnStartup]);
return {
showUpdatePopup,
updateInfo,
isInstalling,
checkForUpdates,
handleUpdateNow,
handleUpdateLater,
handleDismiss,
};
};