import VersionUpdateModal from '@/components/VersionUpdateModal'; import { useToast } from '@/contexts/ToastContext'; import { fetchVersionInfo, getCurrentAppVersion, type VersionInfo } from '@/services/version'; import { log } from '@/utils/logger'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Linking } from 'react-native'; import { useTranslation } from 'react-i18next'; type VersionCheckContextValue = { isChecking: boolean; updateInfo: VersionInfo | null; checkForUpdate: (options?: { manual?: boolean }) => Promise; openStore: () => Promise; }; const VersionCheckContext = createContext(undefined); export function VersionCheckProvider({ children }: { children: React.ReactNode }) { const { showSuccess, showError } = useToast(); const { t } = useTranslation(); const [isChecking, setIsChecking] = useState(false); const [updateInfo, setUpdateInfo] = useState(null); const [modalVisible, setModalVisible] = useState(false); const hasAutoCheckedRef = useRef(false); const currentVersion = useMemo(() => getCurrentAppVersion(), []); const openStore = useCallback(async () => { if (!updateInfo?.appStoreUrl) { showError(t('personal.versionCheck.missingUrl')); return; } try { const supported = await Linking.canOpenURL(updateInfo.appStoreUrl); if (!supported) { throw new Error('URL not supported'); } await Linking.openURL(updateInfo.appStoreUrl); log.info('version-update-open-store', { url: updateInfo.appStoreUrl }); } catch (error) { log.error('version-update-open-store-failed', error); showError(t('personal.versionCheck.openStoreFailed')); } }, [showError, t, updateInfo]); const checkForUpdate = useCallback( async ({ manual = false }: { manual?: boolean } = {}) => { if (isChecking) { if (manual) { showSuccess(t('personal.versionCheck.checking')); } return updateInfo; } setIsChecking(true); try { const info = await fetchVersionInfo(); setUpdateInfo(info); setModalVisible(Boolean(info?.needsUpdate)); if (info?.needsUpdate && manual) { showSuccess( t('personal.versionCheck.updateFound', { version: info.latestVersion, }) ); } else if (!info?.needsUpdate && manual) { showSuccess(t('personal.versionCheck.upToDate')); } return info; } catch (error) { log.error('version-check-failed', error); if (manual) { showError(t('personal.versionCheck.failed')); } return null; } finally { setIsChecking(false); } }, [isChecking, showError, showSuccess, t, updateInfo] ); useEffect(() => { if (hasAutoCheckedRef.current) return; hasAutoCheckedRef.current = true; checkForUpdate({ manual: false }).catch((error) => { log.error('auto-version-check-failed', error); }); }, [checkForUpdate]); const strings = useMemo( () => ({ title: t('personal.versionCheck.modalTitle'), tag: t('personal.versionCheck.modalTag'), currentVersionLabel: t('personal.versionCheck.currentVersion'), latestVersionLabel: t('personal.versionCheck.latestVersion'), updatesTitle: t('personal.versionCheck.releaseNotesTitle'), fallbackNote: t('personal.versionCheck.fallbackNotes'), remindLater: t('personal.versionCheck.later'), updateCta: t('personal.versionCheck.updateNow'), }), [t] ); return ( {children} setModalVisible(false)} onUpdate={openStore} strings={strings} /> ); } export function useVersionCheck(): VersionCheckContextValue { const context = useContext(VersionCheckContext); if (!context) { throw new Error('useVersionCheck must be used within VersionCheckProvider'); } return context; }