UNPKG

@markvivanco/app-version-checker

Version:

React App version checking and update prompts for React, React Native, and web applications

606 lines (596 loc) 19 kB
'use strict'; var React = require('react'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var React__default = /*#__PURE__*/_interopDefault(React); // src/adapters/react/VersionCheckContext.tsx // src/core/types.ts var DEFAULT_CHECK_INTERVALS = { MIN_CHECK_INTERVAL: 60 * 60 * 1e3, // 1 hour minimum between checks REMIND_LATER_DURATION: 24 * 60 * 60 * 1e3 // 24 hours for "remind me later" }; // src/core/version-compare.ts function compareVersions(v1, v2) { const parts1 = v1.split(".").map((num) => parseInt(num, 10)); const parts2 = v2.split(".").map((num) => parseInt(num, 10)); const maxLength = Math.max(parts1.length, parts2.length); while (parts1.length < maxLength) parts1.push(0); while (parts2.length < maxLength) parts2.push(0); for (let i = 0; i < maxLength; i++) { if (parts1[i] < parts2[i]) return -1; if (parts1[i] > parts2[i]) return 1; } return 0; } function isUpdateAvailable(currentVersion, latestVersion) { return compareVersions(currentVersion, latestVersion) < 0; } // src/core/stores.ts function getIosStoreUrl(appStoreId, customUrl) { if (customUrl) { return customUrl; } if (!appStoreId) { return null; } return `https://apps.apple.com/app/id${appStoreId}`; } function getAndroidStoreUrl(packageName, customUrl) { if (customUrl) { return customUrl; } if (!packageName) { return null; } return `https://play.google.com/store/apps/details?id=${packageName}`; } function getStoreUrl(platform, config) { switch (platform) { case "ios": return getIosStoreUrl(config.iosAppStoreId, config.iosStoreUrl); case "android": return getAndroidStoreUrl(config.androidPackageName, config.androidStoreUrl); case "web": return null; default: return null; } } // src/core/version-checker.ts var VersionChecker = class { constructor(dataProvider, storageProvider, options = {}) { this.initialized = false; this.dataProvider = dataProvider; this.storageProvider = storageProvider; this.options = { minCheckInterval: options.minCheckInterval ?? DEFAULT_CHECK_INTERVALS.MIN_CHECK_INTERVAL, remindLaterDuration: options.remindLaterDuration ?? DEFAULT_CHECK_INTERVALS.REMIND_LATER_DURATION, skipWebPlatform: options.skipWebPlatform ?? true, getPlatform: options.getPlatform ?? (() => this.detectPlatform()) }; } /** * Initialize the version checker */ async initialize() { if (this.initialized) { return; } if (this.dataProvider.initialize) { await this.dataProvider.initialize(); } if (this.storageProvider.initialize) { await this.storageProvider.initialize(); } this.initialized = true; } /** * Detect the current platform */ detectPlatform() { if (this.dataProvider.getCurrentPlatform) { return this.dataProvider.getCurrentPlatform(); } if (typeof window !== "undefined") { const userAgent = window.navigator?.userAgent || ""; if (/android/i.test(userAgent)) { return "android"; } if (/iPad|iPhone|iPod/.test(userAgent)) { return "ios"; } return "web"; } return "web"; } /** * Get the current platform */ getPlatform() { return this.options.getPlatform(); } /** * Get version information */ async getVersionInfo() { const platform = this.getPlatform(); const currentVersion = await this.dataProvider.getCurrentVersion(); const latestVersion = await this.dataProvider.getLatestVersion(platform); const appStoreConfig = await this.dataProvider.getAppStoreConfig(); const updateAvailable = latestVersion ? isUpdateAvailable(currentVersion, latestVersion) : false; const storeUrl = getStoreUrl(platform, appStoreConfig); return { currentVersion, latestVersion, updateAvailable, storeUrl, platform }; } /** * Check if an update is available */ async isUpdateAvailable() { const platform = this.getPlatform(); if (platform === "web" && this.options.skipWebPlatform) { return false; } const versionInfo = await this.getVersionInfo(); return versionInfo.updateAvailable; } /** * Check if we should show the update prompt */ async shouldShowUpdatePrompt() { const platform = this.getPlatform(); if (platform === "web" && this.options.skipWebPlatform) { const versionInfo = await this.getVersionInfo(); return { shouldShowPrompt: false, versionInfo, skipReason: "web_platform" }; } try { const versionInfo = await this.getVersionInfo(); if (!versionInfo.updateAvailable) { return { shouldShowPrompt: false, versionInfo, skipReason: "no_update" }; } const remindLaterTime = await this.storageProvider.getRemindLaterTime(); if (remindLaterTime && Date.now() < remindLaterTime) { return { shouldShowPrompt: false, versionInfo, skipReason: "remind_later" }; } const lastCheckTime = await this.storageProvider.getLastCheckTime(); if (lastCheckTime && Date.now() - lastCheckTime < this.options.minCheckInterval) { return { shouldShowPrompt: false, versionInfo, skipReason: "too_soon" }; } if (this.storageProvider.getLastShownVersion) { const lastShownVersion = await this.storageProvider.getLastShownVersion(); if (lastShownVersion === versionInfo.latestVersion) { if (this.dataProvider.isUpdateMandatory) { const isMandatory = await this.dataProvider.isUpdateMandatory( versionInfo.currentVersion, versionInfo.latestVersion ); if (!isMandatory) { return { shouldShowPrompt: false, versionInfo, skipReason: "remind_later" }; } } else { return { shouldShowPrompt: false, versionInfo, skipReason: "remind_later" }; } } } await this.storageProvider.setLastCheckTime(Date.now()); if (this.storageProvider.setLastShownVersion && versionInfo.latestVersion) { await this.storageProvider.setLastShownVersion(versionInfo.latestVersion); } return { shouldShowPrompt: true, versionInfo }; } catch (error) { console.error("Error checking for updates:", error); const versionInfo = await this.getVersionInfo(); return { shouldShowPrompt: false, versionInfo, skipReason: "error" }; } } /** * Set "remind me later" for the update prompt */ async setRemindMeLater() { const remindTime = Date.now() + this.options.remindLaterDuration; await this.storageProvider.setRemindLaterTime(remindTime); if (this.storageProvider.incrementDismissCount) { await this.storageProvider.incrementDismissCount(); } } /** * Clear the "remind me later" setting */ async clearRemindMeLater() { await this.storageProvider.clearRemindLaterTime(); } /** * Check if update is mandatory */ async isUpdateMandatory() { if (!this.dataProvider.isUpdateMandatory) { return false; } const versionInfo = await this.getVersionInfo(); if (!versionInfo.latestVersion) { return false; } return await this.dataProvider.isUpdateMandatory( versionInfo.currentVersion, versionInfo.latestVersion ); } /** * Get changelog for the latest version */ async getChangeLog() { if (!this.dataProvider.getChangeLog) { return null; } const versionInfo = await this.getVersionInfo(); if (!versionInfo.latestVersion) { return null; } return await this.dataProvider.getChangeLog(versionInfo.latestVersion); } /** * Reset all version check data (useful for testing) */ async resetVersionCheckData() { await this.storageProvider.clearRemindLaterTime(); await this.storageProvider.setLastCheckTime(0); if (this.storageProvider.clearAll) { await this.storageProvider.clearAll(); } } /** * Get formatted version string */ async getFormattedVersion() { if (this.dataProvider.getFormattedVersion) { return await this.dataProvider.getFormattedVersion(); } return await this.dataProvider.getCurrentVersion(); } /** * Dispose of resources */ async dispose() { if (this.dataProvider.dispose) { await this.dataProvider.dispose(); } if (this.storageProvider.dispose) { await this.storageProvider.dispose(); } this.initialized = false; } }; // src/adapters/react/VersionCheckContext.tsx var VersionCheckContext = React.createContext(void 0); var VersionCheckProvider = ({ children, dataProvider, storageProvider, options = {}, checkOnMount = true, checkOnForeground = false, onOpenStore, onShowUpdateDialog, onHideUpdateDialog, renderDialog = true, dialogComponent: DialogComponent }) => { const [versionInfo, setVersionInfo] = React.useState(null); const [showUpdateDialog, setShowUpdateDialog] = React.useState(false); const [isChecking, setIsChecking] = React.useState(false); const [error, setError] = React.useState(null); const [currentVersion, setCurrentVersion] = React.useState(null); const [formattedVersion, setFormattedVersion] = React.useState(null); const versionChecker = React.useMemo( () => new VersionChecker(dataProvider, storageProvider, options), [dataProvider, storageProvider, options] ); React.useEffect(() => { versionChecker.initialize().catch(console.error); return () => { versionChecker.dispose().catch(console.error); }; }, [versionChecker]); React.useEffect(() => { const loadVersions = async () => { try { const current = await dataProvider.getCurrentVersion(); setCurrentVersion(current); const formatted = dataProvider.getFormattedVersion ? await dataProvider.getFormattedVersion() : current; setFormattedVersion(formatted); } catch (err) { console.error("Error loading versions:", err); } }; loadVersions(); }, [dataProvider]); const checkForUpdates = React.useCallback(async () => { if (isChecking) return; setIsChecking(true); setError(null); try { const result = await versionChecker.shouldShowUpdatePrompt(); const info = result.versionInfo; setVersionInfo(info); if (result.shouldShowPrompt) { setShowUpdateDialog(true); onShowUpdateDialog?.(info); } } catch (err) { const error2 = err instanceof Error ? err : new Error(String(err)); setError(error2); console.error("Error checking for updates:", error2); } finally { setIsChecking(false); } }, [versionChecker, isChecking, onShowUpdateDialog]); const handleUpdateNow = React.useCallback(async () => { setShowUpdateDialog(false); onHideUpdateDialog?.(); if (versionInfo?.storeUrl) { if (onOpenStore) { await onOpenStore(versionInfo.storeUrl); } else { if (typeof window !== "undefined" && window.open) { window.open(versionInfo.storeUrl, "_blank"); } } } }, [versionInfo, onOpenStore, onHideUpdateDialog]); const handleRemindLater = React.useCallback(async () => { setShowUpdateDialog(false); onHideUpdateDialog?.(); await versionChecker.setRemindMeLater(); }, [versionChecker, onHideUpdateDialog]); const resetVersionCheck = React.useCallback(async () => { await versionChecker.resetVersionCheckData(); setVersionInfo(null); setShowUpdateDialog(false); setError(null); }, [versionChecker]); const getChangeLog = React.useCallback(async () => { return await versionChecker.getChangeLog(); }, [versionChecker]); const isUpdateMandatory = React.useCallback(async () => { return await versionChecker.isUpdateMandatory(); }, [versionChecker]); React.useEffect(() => { if (checkOnMount) { checkForUpdates(); } }, [checkOnMount]); React.useEffect(() => { if (!checkOnForeground) return; }, [checkOnForeground, checkForUpdates]); const contextValue = React.useMemo( () => ({ versionInfo, isUpdateAvailable: versionInfo?.updateAvailable || false, currentVersion, formattedVersion, showUpdateDialog, isChecking, error, checkForUpdates, handleUpdateNow, handleRemindLater, resetVersionCheck, getChangeLog, isUpdateMandatory }), [ versionInfo, currentVersion, formattedVersion, showUpdateDialog, isChecking, error, checkForUpdates, handleUpdateNow, handleRemindLater, resetVersionCheck, getChangeLog, isUpdateMandatory ] ); return /* @__PURE__ */ React__default.default.createElement(VersionCheckContext.Provider, { value: contextValue }, children, renderDialog && DialogComponent && versionInfo && /* @__PURE__ */ React__default.default.createElement( DialogComponent, { visible: showUpdateDialog, versionInfo, onUpdateNow: handleUpdateNow, onRemindLater: handleRemindLater } )); }; var useVersionCheck = () => { const context = React.useContext(VersionCheckContext); if (context === void 0) { throw new Error("useVersionCheck must be used within a VersionCheckProvider"); } return context; }; var useAppStateVersionCheck = (appStateModule, enabled = true) => { const { checkForUpdates } = useVersionCheck(); const [appState, setAppState] = React.useState("active"); React.useEffect(() => { if (!enabled || !appStateModule) return; const currentState = appStateModule.currentState || "active"; setAppState(currentState); const handleAppStateChange = (nextAppState) => { if (appState.match(/inactive|background/) && nextAppState === "active") { checkForUpdates(); } setAppState(nextAppState); }; const subscription = appStateModule.addEventListener("change", handleAppStateChange); return () => { subscription?.remove?.(); }; }, [appState, checkForUpdates, enabled, appStateModule]); return appState; }; var usePeriodicVersionCheck = (intervalMs = 60 * 60 * 1e3, enabled = true) => { const { checkForUpdates } = useVersionCheck(); const intervalRef = React.useRef(null); React.useEffect(() => { if (!enabled) { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } return; } intervalRef.current = setInterval(() => { checkForUpdates(); }, intervalMs); return () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; }, [checkForUpdates, intervalMs, enabled]); }; var useVisibilityVersionCheck = (enabled = true) => { const { checkForUpdates } = useVersionCheck(); React.useEffect(() => { if (!enabled || typeof document === "undefined") return; const handleVisibilityChange = () => { if (!document.hidden) { checkForUpdates(); } }; document.addEventListener("visibilitychange", handleVisibilityChange); return () => { document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, [checkForUpdates, enabled]); }; var useStandaloneVersionChecker = (dataProvider, storageProvider, options) => { const [versionInfo, setVersionInfo] = React.useState(null); const [isChecking, setIsChecking] = React.useState(false); const [error, setError] = React.useState(null); const [showUpdatePrompt, setShowUpdatePrompt] = React.useState(false); const versionCheckerRef = React.useRef(null); React.useEffect(() => { const checker = new VersionChecker(dataProvider, storageProvider); versionCheckerRef.current = checker; checker.initialize().catch(console.error); return () => { checker.dispose().catch(console.error); }; }, [dataProvider, storageProvider]); const checkForUpdates = React.useCallback(async () => { if (!versionCheckerRef.current || isChecking) return; setIsChecking(true); setError(null); try { const result = await versionCheckerRef.current.shouldShowUpdatePrompt(); setVersionInfo(result.versionInfo); setShowUpdatePrompt(result.shouldShowPrompt); } catch (err) { const error2 = err instanceof Error ? err : new Error(String(err)); setError(error2); } finally { setIsChecking(false); } }, [isChecking]); const setRemindMeLater = React.useCallback(async () => { if (!versionCheckerRef.current) return; await versionCheckerRef.current.setRemindMeLater(); setShowUpdatePrompt(false); }, []); React.useEffect(() => { if (options?.checkOnMount !== false) { checkForUpdates(); } }, []); React.useEffect(() => { if (!options?.checkOnFocus) return; const handleFocus = () => checkForUpdates(); window.addEventListener("focus", handleFocus); return () => window.removeEventListener("focus", handleFocus); }, [checkForUpdates, options?.checkOnFocus]); React.useEffect(() => { if (!options?.checkInterval) return; const interval = setInterval(checkForUpdates, options.checkInterval); return () => clearInterval(interval); }, [checkForUpdates, options?.checkInterval]); return { versionInfo, isChecking, error, showUpdatePrompt, checkForUpdates, setRemindMeLater, isUpdateAvailable: versionInfo?.updateAvailable || false }; }; var useVersionInfo = () => { const { versionInfo, currentVersion, formattedVersion } = useVersionCheck(); return { current: currentVersion, latest: versionInfo?.latestVersion, formatted: formattedVersion, updateAvailable: versionInfo?.updateAvailable || false, platform: versionInfo?.platform, storeUrl: versionInfo?.storeUrl }; }; var useUpdateStatus = () => { const { isUpdateAvailable: isUpdateAvailable2, isChecking, error, showUpdateDialog } = useVersionCheck(); return { isUpdateAvailable: isUpdateAvailable2, isChecking, hasError: !!error, error, isDialogVisible: showUpdateDialog }; }; exports.VersionCheckProvider = VersionCheckProvider; exports.useAppStateVersionCheck = useAppStateVersionCheck; exports.usePeriodicVersionCheck = usePeriodicVersionCheck; exports.useStandaloneVersionChecker = useStandaloneVersionChecker; exports.useUpdateStatus = useUpdateStatus; exports.useVersionCheck = useVersionCheck; exports.useVersionInfo = useVersionInfo; exports.useVisibilityVersionCheck = useVisibilityVersionCheck; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map