@markvivanco/app-version-checker
Version:
React App version checking and update prompts for React, React Native, and web applications
444 lines (439 loc) • 13.3 kB
JavaScript
// 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;
}
function parseVersion(version) {
const parts = version.split(".").map((num) => parseInt(num, 10));
return {
major: parts[0] || 0,
minor: parts[1] || 0,
patch: parts[2] || 0,
build: parts[3]
// Optional build number
};
}
function formatVersion(major, minor, patch, build) {
const parts = [major, minor, patch];
if (build !== void 0) {
parts.push(build);
}
return parts.join(".");
}
function getMajorVersion(version) {
return parseVersion(version).major;
}
function getMinorVersion(version) {
return parseVersion(version).minor;
}
function getPatchVersion(version) {
return parseVersion(version).patch;
}
function isValidVersion(version) {
const regex = /^\d+\.\d+\.\d+(\.\d+)?$/;
return regex.test(version);
}
function getVersionDiff(v1, v2) {
const comparison = compareVersions(v1, v2);
if (comparison === 0) {
return { type: "none", fromVersion: v1, toVersion: v2 };
}
const parsed1 = parseVersion(v1);
const parsed2 = parseVersion(v2);
let type = "build";
if (parsed1.major !== parsed2.major) {
type = "major";
} else if (parsed1.minor !== parsed2.minor) {
type = "minor";
} else if (parsed1.patch !== parsed2.patch) {
type = "patch";
}
return {
type,
fromVersion: comparison < 0 ? v1 : v2,
toVersion: comparison < 0 ? v2 : v1
};
}
// src/core/version-formatter.ts
function formatVersionWithBuild(platformVersion, buildNumber) {
if (buildNumber !== void 0 && buildNumber !== null && buildNumber !== "") {
return `${platformVersion}.${buildNumber}`;
}
return platformVersion;
}
function extractBuildNumber(version) {
const parts = version.split(".");
if (parts.length > 3) {
return parts[3];
}
return void 0;
}
function extractBaseVersion(version) {
const parts = version.split(".");
return parts.slice(0, 3).join(".");
}
function normalizeVersion(version, padToLength = 3) {
const parts = version.split(".");
const numericParts = parts.map((part) => parseInt(part, 10)).filter((num) => !isNaN(num));
while (numericParts.length < padToLength) {
numericParts.push(0);
}
return numericParts.join(".");
}
function formatDisplayVersion(version, includePrefix = true) {
const normalized = normalizeVersion(version, 3);
return includePrefix ? `v${normalized}` : normalized;
}
function sortVersions(versions, descending = true) {
const sorted = [...versions].sort((a, b) => {
const aParts = a.split(".").map((num) => parseInt(num, 10));
const bParts = b.split(".").map((num) => parseInt(num, 10));
const maxLength = Math.max(aParts.length, bParts.length);
for (let i = 0; i < maxLength; i++) {
const aPart = aParts[i] || 0;
const bPart = bParts[i] || 0;
if (aPart !== bPart) {
return descending ? bPart - aPart : aPart - bPart;
}
}
return 0;
});
return sorted;
}
function getLatestVersion(versions) {
if (!versions || versions.length === 0) {
return null;
}
const sorted = sortVersions(versions, true);
return sorted[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;
}
}
function isValidIosAppStoreId(appStoreId) {
return /^\d{9,10}$/.test(appStoreId);
}
function isValidAndroidPackageName(packageName) {
return /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(packageName);
}
function extractAppIdFromUrl(url, platform) {
if (platform === "ios") {
const match = url.match(/\/id(\d+)/);
return match ? match[1] : null;
}
if (platform === "android") {
const match = url.match(/[?&]id=([^&]+)/);
return match ? match[1] : null;
}
return null;
}
function getStoreName(platform) {
switch (platform) {
case "ios":
return "App Store";
case "android":
return "Google Play Store";
case "web":
return "Web";
default:
return "Unknown";
}
}
function getStoreBadgeUrl(platform, _locale = "en-US") {
switch (platform) {
case "ios":
return `https://developer.apple.com/app-store/marketing/guidelines/images/badge-download-on-the-app-store.svg`;
case "android":
return `https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png`;
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;
}
};
export { DEFAULT_CHECK_INTERVALS, VersionChecker, compareVersions, extractAppIdFromUrl, extractBaseVersion, extractBuildNumber, formatDisplayVersion, formatVersion, formatVersionWithBuild, getAndroidStoreUrl, getIosStoreUrl, getLatestVersion, getMajorVersion, getMinorVersion, getPatchVersion, getStoreBadgeUrl, getStoreName, getStoreUrl, getVersionDiff, isUpdateAvailable, isValidAndroidPackageName, isValidIosAppStoreId, isValidVersion, normalizeVersion, parseVersion, sortVersions };
//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map