diff --git a/src/hooks/useGithubMajorUpdate.ts b/src/hooks/useGithubMajorUpdate.ts index 1312579d..00e0e4ba 100644 --- a/src/hooks/useGithubMajorUpdate.ts +++ b/src/hooks/useGithubMajorUpdate.ts @@ -26,6 +26,12 @@ export function useGithubMajorUpdate(): MajorUpdateData { const check = useCallback(async () => { if (Platform.OS === 'ios') return; try { + // Check if major update alerts are disabled + const majorAlertsEnabled = await mmkvStorage.getItem('@major_updates_alerts_enabled'); + if (majorAlertsEnabled === 'false') { + return; // Major update alerts are disabled by user + } + // Always compare with Settings screen version const current = getDisplayedAppVersion() || Updates.runtimeVersion || '0.0.0'; const info = await fetchLatestGithubRelease(); diff --git a/src/hooks/useUpdatePopup.ts b/src/hooks/useUpdatePopup.ts index b7caaea8..662971f8 100644 --- a/src/hooks/useUpdatePopup.ts +++ b/src/hooks/useUpdatePopup.ts @@ -28,6 +28,11 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { const checkForUpdates = useCallback(async (forceCheck = false) => { try { + // Check if OTA update alerts are disabled + const otaAlertsEnabled = await mmkvStorage.getItem('@ota_updates_alerts_enabled'); + if (otaAlertsEnabled === 'false' && !forceCheck) { + return; // OTA alerts are disabled by user + } // Check if user has dismissed the popup for this version const dismissedVersion = await mmkvStorage.getItem(UPDATE_POPUP_STORAGE_KEY); @@ -66,9 +71,9 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { 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 @@ -152,7 +157,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { try { const dismissedVersion = await mmkvStorage.getItem(UPDATE_POPUP_STORAGE_KEY); const currentVersion = updateInfo.manifest?.id; - + if (dismissedVersion !== currentVersion) { setShowUpdatePopup(true); } diff --git a/src/screens/UpdateScreen.tsx b/src/screens/UpdateScreen.tsx index f3345e87..92735428 100644 --- a/src/screens/UpdateScreen.tsx +++ b/src/screens/UpdateScreen.tsx @@ -9,7 +9,8 @@ import { StatusBar, Platform, Dimensions, - Linking + Linking, + Switch } from 'react-native'; import { useToast } from '../contexts/ToastContext'; import { useNavigation } from '@react-navigation/native'; @@ -37,9 +38,9 @@ interface SettingsCardProps { const SettingsCard: React.FC = ({ children, title, isTablet = false }) => { const { currentTheme } = useTheme(); - + return ( - { const [updateProgress, setUpdateProgress] = useState(0); const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'available' | 'downloading' | 'installing' | 'success' | 'error'>('idle'); + // Update notification settings + const [otaAlertsEnabled, setOtaAlertsEnabled] = useState(true); + const [majorAlertsEnabled, setMajorAlertsEnabled] = useState(true); + + // Load notification settings on mount + useEffect(() => { + (async () => { + try { + const otaSetting = await mmkvStorage.getItem('@ota_updates_alerts_enabled'); + const majorSetting = await mmkvStorage.getItem('@major_updates_alerts_enabled'); + // Default to true if not set + setOtaAlertsEnabled(otaSetting !== 'false'); + setMajorAlertsEnabled(majorSetting !== 'false'); + } catch { } + })(); + }, []); + + // Handle toggling OTA alerts with warning + const handleOtaAlertsToggle = async (value: boolean) => { + if (!value) { + openAlert( + 'Disable OTA Update Alerts?', + 'You will no longer receive automatic notifications for OTA updates.\n\n⚠️ Warning: Staying on the latest version is important for:\n• Bug fixes and stability improvements\n• New features and enhancements\n• Providing accurate feedback and crash reports\n\nYou can still manually check for updates in this screen.', + [ + { label: 'Cancel', onPress: () => setAlertVisible(false) }, + { + label: 'Disable', + onPress: async () => { + await mmkvStorage.setItem('@ota_updates_alerts_enabled', 'false'); + setOtaAlertsEnabled(false); + setAlertVisible(false); + } + } + ] + ); + } else { + await mmkvStorage.setItem('@ota_updates_alerts_enabled', 'true'); + setOtaAlertsEnabled(true); + } + }; + + // Handle toggling Major update alerts with warning + const handleMajorAlertsToggle = async (value: boolean) => { + if (!value) { + openAlert( + 'Disable Major Update Alerts?', + 'You will no longer receive notifications for major app updates that require reinstallation.\n\n⚠️ Warning: Major updates often include:\n• Critical security patches\n• Breaking changes that require app reinstall\n• Important compatibility fixes\n\nYou can still check for updates manually.', + [ + { label: 'Cancel', onPress: () => setAlertVisible(false) }, + { + label: 'Disable', + onPress: async () => { + await mmkvStorage.setItem('@major_updates_alerts_enabled', 'false'); + setMajorAlertsEnabled(false); + setAlertVisible(false); + } + } + ] + ); + } else { + await mmkvStorage.setItem('@major_updates_alerts_enabled', 'true'); + setMajorAlertsEnabled(true); + } + }; + const checkForUpdates = async () => { try { setIsChecking(true); setUpdateStatus('checking'); setUpdateProgress(0); setLastOperation('Checking for updates...'); - + const info = await UpdateService.checkForUpdates(); setUpdateInfo(info); setLastChecked(new Date()); - + // Logs disabled - + if (info.isAvailable) { setUpdateStatus('available'); setLastOperation(`Update available: ${info.manifest?.id || 'unknown'}`); @@ -135,7 +201,7 @@ const UpdateScreen: React.FC = () => { if (__DEV__) console.error('Error checking for updates:', error); setUpdateStatus('error'); setLastOperation(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); - openAlert('Error', 'Failed to check for updates'); + openAlert('Error', 'Failed to check for updates'); } finally { setIsChecking(false); } @@ -146,12 +212,12 @@ const UpdateScreen: React.FC = () => { if (Platform.OS === 'android') { // ensure badge clears when entering this screen (async () => { - try { await mmkvStorage.removeItem('@update_badge_pending'); } catch {} + try { await mmkvStorage.removeItem('@update_badge_pending'); } catch { } })(); } checkForUpdates(); // Also refresh GitHub section on mount (works in dev and prod) - try { github.refresh(); } catch {} + try { github.refresh(); } catch { } if (Platform.OS === 'android') { showInfo('Checking for Updates', 'Checking for updates…'); } @@ -163,7 +229,7 @@ const UpdateScreen: React.FC = () => { setUpdateStatus('downloading'); setUpdateProgress(0); setLastOperation('Downloading update...'); - + // Simulate progress updates const progressInterval = setInterval(() => { setUpdateProgress(prev => { @@ -171,30 +237,30 @@ const UpdateScreen: React.FC = () => { return prev + Math.random() * 10; }); }, 500); - + const success = await UpdateService.downloadAndInstallUpdate(); - + clearInterval(progressInterval); setUpdateProgress(100); setUpdateStatus('installing'); setLastOperation('Installing update...'); - + // Logs disabled - + if (success) { setUpdateStatus('success'); setLastOperation('Update installed successfully'); - openAlert('Success', 'Update will be applied on next app restart'); + openAlert('Success', 'Update will be applied on next app restart'); } else { setUpdateStatus('error'); setLastOperation('No update available to install'); - openAlert('No Update', 'No update available to install'); + openAlert('No Update', 'No update available to install'); } } catch (error) { if (__DEV__) console.error('Error installing update:', error); setUpdateStatus('error'); setLastOperation(`Installation error: ${error instanceof Error ? error.message : 'Unknown error'}`); - openAlert('Error', 'Failed to install update'); + openAlert('Error', 'Failed to install update'); } finally { setIsInstalling(false); } @@ -236,7 +302,7 @@ const UpdateScreen: React.FC = () => { try { setLastOperation('Testing connectivity...'); const isReachable = await UpdateService.testUpdateConnectivity(); - + if (isReachable) { setLastOperation('Update server is reachable'); } else { @@ -334,7 +400,7 @@ const UpdateScreen: React.FC = () => { { backgroundColor: currentTheme.colors.darkBackground } ]}> - + { Settings - + {/* Empty for now, but ready for future actions */} - + App Updates - - - - {/* Main Update Card */} - - {/* Status Section */} - - - {getStatusIcon()} - - - - {getStatusText()} - - - {lastOperation || 'Ready to check for updates'} - - + + + + {/* Main Update Card */} + + {/* Status Section */} + + + {getStatusIcon()} - - {/* Progress Section */} - {(updateStatus === 'downloading' || updateStatus === 'installing') && ( - - - - {updateStatus === 'downloading' ? 'Downloading' : 'Installing'} - - - {Math.round(updateProgress)}% - - - - - - - )} - - {/* Action Section */} - - - {isChecking ? ( - - ) : ( - - )} - - {isChecking ? 'Checking...' : 'Check for Updates'} - - - - {updateInfo?.isAvailable && updateStatus !== 'success' && ( - - {isInstalling ? ( - - ) : ( - - )} - - {isInstalling ? 'Installing...' : 'Install Update'} - - - )} - + + + {getStatusText()} + + + {lastOperation || 'Ready to check for updates'} + - {/* Release Notes */} - {updateInfo?.isAvailable && !!getReleaseNotes() && ( - - - - - - Release notes: + {/* Progress Section */} + {(updateStatus === 'downloading' || updateStatus === 'installing') && ( + + + + {updateStatus === 'downloading' ? 'Downloading' : 'Installing'} + + + {Math.round(updateProgress)}% + + + + - {getReleaseNotes()} )} - {/* Info Section */} - - - - - - Version: - - {updateInfo?.manifest?.id ? `${updateInfo.manifest.id.substring(0, 8)}...` : 'Unknown'} + {/* Action Section */} + + + {isChecking ? ( + + ) : ( + + )} + + {isChecking ? 'Checking...' : 'Check for Updates'} - - - {lastChecked && ( - - - - - Last checked: - - {formatDate(lastChecked)} - - - )} - + - {/* Current Version Section */} - - - - - - Current version: - - {currentInfo?.manifest?.id || (currentInfo?.isEmbeddedLaunch === false ? 'Unknown' : 'Embedded')} - - - - {!!getCurrentReleaseNotes() && ( - - - - - - Current release notes: - - - {getCurrentReleaseNotes()} - - - )} - - - {/* Developer Logs removed */} - - - {/* GitHub Release (compact) – only show when update is available */} - {github.latestTag && isAnyUpgrade(getDisplayedAppVersion(), github.latestTag) ? ( - - - - - - - Current: - - {getDisplayedAppVersion()} - - - - - - - - Latest: - - {github.latestTag} - - - - {github.releaseNotes ? ( - - Notes: - - {github.releaseNotes} - - - ) : null} - - - - github.releaseUrl ? Linking.openURL(github.releaseUrl as string) : null} - activeOpacity={0.8} - > - - View Release - - - - - - ) : null} - - {false && ( - - - - - Update Service Logs - - - - - - - - - {/* Test log removed */} - {/* Copy all logs removed */} - {/* Refresh logs removed */} - {/* Clear logs removed */} - - - - - {false ? ( - No logs available + {isInstalling ? ( + ) : ( - ([] as string[]).map((log, index) => { - const isError = log.indexOf('[ERROR]') !== -1; - const isWarning = log.indexOf('[WARN]') !== -1; - - return ( - {}} - activeOpacity={0.7} - > - - - {log} - - - - - ); - }) + )} - + + {isInstalling ? 'Installing...' : 'Install Update'} + + + )} + + + + + {/* Release Notes */} + {updateInfo?.isAvailable && !!getReleaseNotes() && ( + + + + + + Release notes: - + {getReleaseNotes()} + )} - - + + {/* Info Section */} + + + + + + Version: + + {updateInfo?.manifest?.id ? `${updateInfo.manifest.id.substring(0, 8)}...` : 'Unknown'} + + + + {lastChecked && ( + + + + + Last checked: + + {formatDate(lastChecked)} + + + )} + + + {/* Current Version Section */} + + + + + + Current version: + + {currentInfo?.manifest?.id || (currentInfo?.isEmbeddedLaunch === false ? 'Unknown' : 'Embedded')} + + + + {!!getCurrentReleaseNotes() && ( + + + + + + Current release notes: + + + {getCurrentReleaseNotes()} + + + )} + + + {/* Developer Logs removed */} + + + {/* GitHub Release (compact) – only show when update is available */} + {github.latestTag && isAnyUpgrade(getDisplayedAppVersion(), github.latestTag) ? ( + + + + + + + Current: + + {getDisplayedAppVersion()} + + + + + + + + Latest: + + {github.latestTag} + + + + {github.releaseNotes ? ( + + Notes: + + {github.releaseNotes} + + + ) : null} + + + + github.releaseUrl ? Linking.openURL(github.releaseUrl as string) : null} + activeOpacity={0.8} + > + + View Release + + + + + + ) : null} + + {/* Update Notification Settings */} + + {/* OTA Updates Toggle */} + + + + OTA Update Alerts + + + Show notifications for over-the-air updates + + + + + + {/* Major Updates Toggle */} + + + + Major Update Alerts + + + Show notifications for new app versions on GitHub + + + + + + {/* Warning note */} + + + + + + Keeping alerts enabled ensures you receive bug fixes and can provide accurate crash reports. + + + + + {false && ( + + + + + Update Service Logs + + + + + + + + + {/* Test log removed */} + {/* Copy all logs removed */} + {/* Refresh logs removed */} + {/* Clear logs removed */} + + + + + {false ? ( + No logs available + ) : ( + ([] as string[]).map((log, index) => { + const isError = log.indexOf('[ERROR]') !== -1; + const isWarning = log.indexOf('[WARN]') !== -1; + + return ( + { }} + activeOpacity={0.7} + > + + + {log} + + + + + ); + }) + )} + + + + )} + +