@lucidaquarian/app-version-checker 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js ADDED
@@ -0,0 +1,265 @@
1
+ /**
2
+ * @lucidaquarian/app-version-checker
3
+ *
4
+ * A cross-platform version checker for Cordova and Capacitor mobile applications.
5
+ * Detects the current app version, compares it against the latest published version
6
+ * (from the App Store, Google Play, or a custom endpoint), and returns actionable
7
+ * update information.
8
+ *
9
+ * @example
10
+ * const { AppVersionChecker } = require('@lucidaquarian/app-version-checker');
11
+ *
12
+ * const checker = new AppVersionChecker({
13
+ * platform: 'auto', // 'cordova' | 'capacitor' | 'auto'
14
+ * iosBundleId: 'com.example.myapp', // for App Store lookup
15
+ * androidPackageName: 'com.example.myapp', // for Play Store lookup
16
+ * country: 'us', // App Store region
17
+ * });
18
+ *
19
+ * const result = await checker.checkForUpdate();
20
+ * if (result.updateAvailable) {
21
+ * console.log(`New version ${result.latestVersion} available!`);
22
+ * }
23
+ */
24
+
25
+ const SemVer = require('./semver');
26
+ const CordovaProvider = require('./providers/cordova');
27
+ const CapacitorProvider = require('./providers/capacitor');
28
+ const {
29
+ fetchAppStoreVersion,
30
+ fetchPlayStoreVersion,
31
+ fetchCustomEndpoint,
32
+ } = require('./stores');
33
+
34
+ // ─── Default Configuration ───────────────────────────────────────────
35
+
36
+ const DEFAULT_CONFIG = {
37
+ /** @type {'cordova' | 'capacitor' | 'auto'} */
38
+ platform: 'auto',
39
+
40
+ /** iOS bundle identifier for App Store lookup */
41
+ iosBundleId: '',
42
+
43
+ /** Android package name for Play Store lookup */
44
+ androidPackageName: '',
45
+
46
+ /** Two-letter country code for App Store */
47
+ country: 'us',
48
+
49
+ /**
50
+ * Optional custom endpoint URL that returns JSON with a `version` field.
51
+ * When set, the store lookup is skipped in favor of this endpoint.
52
+ */
53
+ customEndpoint: '',
54
+
55
+ /** Extra fetch options passed to the custom endpoint request */
56
+ customEndpointOptions: {},
57
+
58
+ /**
59
+ * If set, any installed version below this is treated as a forced update,
60
+ * regardless of what the store says. Overridden by the remote `minVersion`.
61
+ */
62
+ minVersion: '',
63
+
64
+ /** Network timeout (ms) for store / custom-endpoint requests. */
65
+ timeout: 10000,
66
+ };
67
+
68
+ // ─── Main Class ──────────────────────────────────────────────────────
69
+
70
+ class AppVersionChecker {
71
+ /**
72
+ * @param {Partial<typeof DEFAULT_CONFIG>} config
73
+ */
74
+ constructor(config = {}) {
75
+ /** @type {typeof DEFAULT_CONFIG} */
76
+ this.config = { ...DEFAULT_CONFIG, ...config };
77
+
78
+ this._cordova = new CordovaProvider();
79
+ this._capacitor = new CapacitorProvider();
80
+ this._provider = null;
81
+ }
82
+
83
+ // ── Provider Detection ─────────────────────────────────────────────
84
+
85
+ /**
86
+ * Detect and return the appropriate platform provider.
87
+ * @returns {CordovaProvider | CapacitorProvider}
88
+ */
89
+ getProvider() {
90
+ if (this._provider) return this._provider;
91
+
92
+ const { platform } = this.config;
93
+
94
+ if (platform === 'cordova') {
95
+ this._provider = this._cordova;
96
+ } else if (platform === 'capacitor') {
97
+ this._provider = this._capacitor;
98
+ } else {
99
+ // Auto-detect: prefer Capacitor (newer), fall back to Cordova
100
+ if (this._capacitor.isAvailable()) {
101
+ this._provider = this._capacitor;
102
+ } else if (this._cordova.isAvailable()) {
103
+ this._provider = this._cordova;
104
+ } else {
105
+ throw new Error(
106
+ 'No mobile platform detected. ' +
107
+ 'Ensure Cordova or Capacitor is initialized, or set the "platform" config explicitly.'
108
+ );
109
+ }
110
+ }
111
+
112
+ return this._provider;
113
+ }
114
+
115
+ /**
116
+ * Get the name of the detected platform.
117
+ * @returns {string}
118
+ */
119
+ getPlatformName() {
120
+ return this.getProvider().name;
121
+ }
122
+
123
+ // ── Local Version ──────────────────────────────────────────────────
124
+
125
+ /**
126
+ * Get the currently installed app version.
127
+ * @returns {Promise<string>}
128
+ */
129
+ async getCurrentVersion() {
130
+ return this.getProvider().getVersion();
131
+ }
132
+
133
+ /**
134
+ * Get the full app info object from the platform.
135
+ * @returns {Promise<object>}
136
+ */
137
+ async getAppInfo() {
138
+ return this.getProvider().getAppInfo();
139
+ }
140
+
141
+ /**
142
+ * Get the current OS-level platform (ios / android / web).
143
+ * @returns {string}
144
+ */
145
+ getDevicePlatform() {
146
+ return this.getProvider().getPlatform();
147
+ }
148
+
149
+ // ── Remote Version Lookup ──────────────────────────────────────────
150
+
151
+ /**
152
+ * Fetch the latest version info from the appropriate store or custom endpoint.
153
+ * @returns {Promise<{ version: string, storeUrl?: string, releaseNotes?: string, minVersion?: string, forceUpdate?: boolean }>}
154
+ */
155
+ async getLatestVersion() {
156
+ const { timeout } = this.config;
157
+
158
+ // Prefer custom endpoint if configured
159
+ if (this.config.customEndpoint) {
160
+ return fetchCustomEndpoint(
161
+ this.config.customEndpoint,
162
+ this.config.customEndpointOptions,
163
+ timeout
164
+ );
165
+ }
166
+
167
+ const devicePlatform = this.getDevicePlatform();
168
+
169
+ if (devicePlatform === 'ios') {
170
+ const bundleId = this.config.iosBundleId;
171
+ if (!bundleId) {
172
+ throw new Error('iosBundleId must be set in config for App Store lookups.');
173
+ }
174
+ return fetchAppStoreVersion(bundleId, this.config.country, timeout);
175
+ }
176
+
177
+ if (devicePlatform === 'android') {
178
+ const packageName = this.config.androidPackageName;
179
+ if (!packageName) {
180
+ throw new Error('androidPackageName must be set in config for Play Store lookups.');
181
+ }
182
+ return fetchPlayStoreVersion(packageName, timeout);
183
+ }
184
+
185
+ throw new Error(
186
+ `Unsupported device platform "${devicePlatform}". Use a customEndpoint for web-based checks.`
187
+ );
188
+ }
189
+
190
+ // ── Update Check ───────────────────────────────────────────────────
191
+
192
+ /**
193
+ * Perform a full version check: compare local version against latest remote version.
194
+ *
195
+ * @returns {Promise<VersionCheckResult>}
196
+ *
197
+ * @typedef {Object} VersionCheckResult
198
+ * @property {string} currentVersion - The installed app version
199
+ * @property {string} latestVersion - The latest available version
200
+ * @property {boolean} updateAvailable - True if a newer version exists
201
+ * @property {'major'|'minor'|'patch'|'prerelease'|'none'} updateType - Kind of update
202
+ * @property {boolean} forceUpdate - True if the current version is below the minimum
203
+ * @property {string} storeUrl - URL to the store listing (if available)
204
+ * @property {string} releaseNotes - Release notes (if available)
205
+ * @property {string} platform - Detected platform name
206
+ */
207
+ async checkForUpdate() {
208
+ const [currentVersion, remoteInfo] = await Promise.all([
209
+ this.getCurrentVersion(),
210
+ this.getLatestVersion(),
211
+ ]);
212
+
213
+ const latestVersion = remoteInfo.version;
214
+ const updateAvailable = SemVer.lt(currentVersion, latestVersion);
215
+ const updateType = SemVer.updateType(currentVersion, latestVersion);
216
+
217
+ // Determine forced update: use remote minVersion if provided, else local config
218
+ const effectiveMinVersion = remoteInfo.minVersion || this.config.minVersion || '';
219
+ let forceUpdate = remoteInfo.forceUpdate || false;
220
+ if (effectiveMinVersion && SemVer.lt(currentVersion, effectiveMinVersion)) {
221
+ forceUpdate = true;
222
+ }
223
+
224
+ return {
225
+ currentVersion,
226
+ latestVersion,
227
+ updateAvailable,
228
+ updateType,
229
+ forceUpdate,
230
+ storeUrl: remoteInfo.storeUrl || '',
231
+ releaseNotes: remoteInfo.releaseNotes || '',
232
+ platform: this.getPlatformName(),
233
+ devicePlatform: this.getDevicePlatform(),
234
+ };
235
+ }
236
+
237
+ // ── Static Convenience ─────────────────────────────────────────────
238
+
239
+ /**
240
+ * Compare two arbitrary version strings without any platform dependency.
241
+ * Useful for quick checks or unit tests.
242
+ *
243
+ * @param {string} currentVersion
244
+ * @param {string} latestVersion
245
+ * @returns {{ updateAvailable: boolean, updateType: string }}
246
+ */
247
+ static compareVersions(currentVersion, latestVersion) {
248
+ return {
249
+ updateAvailable: SemVer.lt(currentVersion, latestVersion),
250
+ updateType: SemVer.updateType(currentVersion, latestVersion),
251
+ };
252
+ }
253
+ }
254
+
255
+ // ─── Exports ─────────────────────────────────────────────────────────
256
+
257
+ module.exports = {
258
+ AppVersionChecker,
259
+ SemVer,
260
+ CordovaProvider,
261
+ CapacitorProvider,
262
+ fetchAppStoreVersion,
263
+ fetchPlayStoreVersion,
264
+ fetchCustomEndpoint,
265
+ };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Capacitor platform provider.
3
+ * Retrieves the current app version using @capacitor/app or @capacitor/device APIs.
4
+ */
5
+
6
+ class CapacitorProvider {
7
+ constructor() {
8
+ this.name = 'capacitor';
9
+ this._appPlugin = null;
10
+ this._devicePlugin = null;
11
+ }
12
+
13
+ /**
14
+ * Check if we are running inside a Capacitor environment.
15
+ * @returns {boolean}
16
+ */
17
+ isAvailable() {
18
+ return (
19
+ typeof window !== 'undefined' &&
20
+ typeof window.Capacitor !== 'undefined' &&
21
+ window.Capacitor.isNativePlatform !== undefined
22
+ );
23
+ }
24
+
25
+ /**
26
+ * Check if the app is running on a native device (not web).
27
+ * @returns {boolean}
28
+ */
29
+ isNative() {
30
+ return this.isAvailable() && window.Capacitor.isNativePlatform();
31
+ }
32
+
33
+ /**
34
+ * Dynamically import the @capacitor/app plugin.
35
+ * @returns {Promise<any>}
36
+ * @private
37
+ */
38
+ async _getAppPlugin() {
39
+ if (this._appPlugin) return this._appPlugin;
40
+
41
+ try {
42
+ // Try Capacitor 4+ / 5+ dynamic import
43
+ const mod = await import('@capacitor/app');
44
+ this._appPlugin = mod.App;
45
+ return this._appPlugin;
46
+ } catch {
47
+ // Fallback: check global Capacitor Plugins
48
+ if (window.Capacitor?.Plugins?.App) {
49
+ this._appPlugin = window.Capacitor.Plugins.App;
50
+ return this._appPlugin;
51
+ }
52
+ throw new Error(
53
+ '@capacitor/app plugin is not installed. ' +
54
+ 'Install it with: npm install @capacitor/app'
55
+ );
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Dynamically import the @capacitor/device plugin.
61
+ * @returns {Promise<any>}
62
+ * @private
63
+ */
64
+ async _getDevicePlugin() {
65
+ if (this._devicePlugin) return this._devicePlugin;
66
+
67
+ try {
68
+ const mod = await import('@capacitor/device');
69
+ this._devicePlugin = mod.Device;
70
+ return this._devicePlugin;
71
+ } catch {
72
+ if (window.Capacitor?.Plugins?.Device) {
73
+ this._devicePlugin = window.Capacitor.Plugins.Device;
74
+ return this._devicePlugin;
75
+ }
76
+ // Device plugin is optional, return null
77
+ return null;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get the current app version info from Capacitor.
83
+ *
84
+ * @returns {Promise<{ versionName: string, versionCode: string, platform: string }>}
85
+ */
86
+ async getAppInfo() {
87
+ if (!this.isAvailable()) {
88
+ throw new Error(
89
+ 'Capacitor is not available. Ensure this is running inside a Capacitor app.'
90
+ );
91
+ }
92
+
93
+ const App = await this._getAppPlugin();
94
+ const info = await App.getInfo();
95
+
96
+ return {
97
+ versionName: info.version, // e.g. "1.2.3"
98
+ versionCode: info.build, // e.g. "42"
99
+ packageName: info.id, // e.g. "com.example.app"
100
+ appName: info.name, // e.g. "My App"
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Get only the version string (e.g. "1.2.3").
106
+ * @returns {Promise<string>}
107
+ */
108
+ async getVersion() {
109
+ const info = await this.getAppInfo();
110
+ return info.versionName;
111
+ }
112
+
113
+ /**
114
+ * Get the build/version code.
115
+ * @returns {Promise<string>}
116
+ */
117
+ async getBuildNumber() {
118
+ const info = await this.getAppInfo();
119
+ return info.versionCode;
120
+ }
121
+
122
+ /**
123
+ * Get the current platform ("ios", "android", or "web").
124
+ * @returns {string}
125
+ */
126
+ getPlatform() {
127
+ if (!this.isAvailable()) return 'unknown';
128
+ return window.Capacitor.getPlatform();
129
+ }
130
+
131
+ /**
132
+ * Get device information (OS version, model, etc.).
133
+ * Requires @capacitor/device plugin.
134
+ * @returns {Promise<object|null>}
135
+ */
136
+ async getDeviceInfo() {
137
+ const Device = await this._getDevicePlugin();
138
+ if (!Device) return null;
139
+ return Device.getInfo();
140
+ }
141
+ }
142
+
143
+ module.exports = CapacitorProvider;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Cordova platform provider.
3
+ * Retrieves the current app version using the cordova-plugin-app-version API.
4
+ */
5
+
6
+ class CordovaProvider {
7
+ constructor() {
8
+ this.name = 'cordova';
9
+ }
10
+
11
+ /**
12
+ * Check if we are running inside a Cordova environment.
13
+ * @returns {boolean}
14
+ */
15
+ isAvailable() {
16
+ return (
17
+ typeof window !== 'undefined' &&
18
+ typeof window.cordova !== 'undefined'
19
+ );
20
+ }
21
+
22
+ /**
23
+ * Get the current app version from Cordova.
24
+ * Requires `cordova-plugin-app-version` to be installed.
25
+ *
26
+ * @returns {Promise<{ versionName: string, versionCode: string|number, packageName: string, appName: string }>}
27
+ */
28
+ async getAppInfo() {
29
+ if (!this.isAvailable()) {
30
+ throw new Error(
31
+ 'Cordova is not available. Ensure this is running inside a Cordova app.'
32
+ );
33
+ }
34
+
35
+ await this._waitForDeviceReady();
36
+
37
+ const cordova = window.cordova;
38
+
39
+ // Check for cordova-plugin-app-version
40
+ if (
41
+ typeof cordova.getAppVersion === 'undefined' &&
42
+ typeof window.AppVersion === 'undefined'
43
+ ) {
44
+ throw new Error(
45
+ 'cordova-plugin-app-version is not installed. ' +
46
+ 'Install it with: cordova plugin add cordova-plugin-app-version'
47
+ );
48
+ }
49
+
50
+ const appVersion = cordova.getAppVersion || window.AppVersion;
51
+
52
+ // Bind each method to the plugin object so callers that rely on `this`
53
+ // inside the plugin keep working when the function is passed to _promisify.
54
+ const [versionName, versionCode, packageName, appName] = await Promise.all([
55
+ this._promisify(appVersion.getVersionNumber.bind(appVersion)),
56
+ this._promisify(appVersion.getVersionCode.bind(appVersion)),
57
+ this._promisify(appVersion.getPackageName.bind(appVersion)),
58
+ this._promisify(appVersion.getAppName.bind(appVersion)),
59
+ ]);
60
+
61
+ return { versionName, versionCode, packageName, appName };
62
+ }
63
+
64
+ /**
65
+ * Get only the version string (e.g. "1.2.3").
66
+ * @returns {Promise<string>}
67
+ */
68
+ async getVersion() {
69
+ const info = await this.getAppInfo();
70
+ return info.versionName;
71
+ }
72
+
73
+ /**
74
+ * Get the build/version code (e.g. 42).
75
+ * @returns {Promise<string|number>}
76
+ */
77
+ async getBuildNumber() {
78
+ const info = await this.getAppInfo();
79
+ return info.versionCode;
80
+ }
81
+
82
+ /**
83
+ * Get the current platform (ios or android).
84
+ * @returns {string}
85
+ */
86
+ getPlatform() {
87
+ if (!this.isAvailable()) return 'unknown';
88
+ return window.cordova.platformId || window.device?.platform?.toLowerCase() || 'unknown';
89
+ }
90
+
91
+ /**
92
+ * Wait for the Cordova deviceready event.
93
+ * @returns {Promise<void>}
94
+ * @private
95
+ */
96
+ _waitForDeviceReady() {
97
+ return new Promise((resolve) => {
98
+ if (
99
+ typeof document === 'undefined' ||
100
+ document.readyState === 'complete' ||
101
+ window.cordova?.plugins
102
+ ) {
103
+ resolve();
104
+ return;
105
+ }
106
+
107
+ let settled = false;
108
+ let timerId = null;
109
+ const done = () => {
110
+ if (settled) return;
111
+ settled = true;
112
+ if (timerId !== null) clearTimeout(timerId);
113
+ document.removeEventListener('deviceready', done, false);
114
+ resolve();
115
+ };
116
+
117
+ document.addEventListener('deviceready', done, false);
118
+
119
+ // Safety net: `deviceready` fires only once, so if it already fired
120
+ // before this listener was attached we would wait forever. Resolve
121
+ // after a bounded delay rather than hang the update check.
122
+ timerId = setTimeout(done, 5000);
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Convert a Cordova callback-style function to a Promise.
128
+ * @param {Function} fn
129
+ * @returns {Promise<any>}
130
+ * @private
131
+ */
132
+ _promisify(fn) {
133
+ return new Promise((resolve, reject) => {
134
+ try {
135
+ fn(resolve, reject);
136
+ } catch (err) {
137
+ reject(err);
138
+ }
139
+ });
140
+ }
141
+ }
142
+
143
+ module.exports = CordovaProvider;