@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
JavaScript
'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