UNPKG

native-update

Version:

Foundation package for building a comprehensive update system for Capacitor apps. Provides architecture and interfaces but requires backend implementation.

595 lines 23.5 kB
import { WebPlugin } from '@capacitor/core'; import { SyncStatus, BundleStatus, UpdateErrorCode } from './definitions'; export class NativeUpdateWeb extends WebPlugin { constructor() { super(); this.config = {}; this.currentBundle = null; this.bundles = new Map(); this.lastReviewRequest = 0; this.launchCount = 0; this.backgroundUpdateStatus = { enabled: false, isRunning: false, checkCount: 0, failureCount: 0, }; this.backgroundCheckInterval = null; this.loadStoredData(); this.incrementLaunchCount(); } /** * Configuration and Core Methods */ async configure(options) { // Store the plugin config // In a real implementation, this would configure the plugin properly if (options.config) { this.saveConfiguration(); } // console.log('NativeUpdate configured:', options.config); } async getSecurityInfo() { var _a, _b, _c, _d, _e, _f, _g; return { enforceHttps: ((_a = this.config.security) === null || _a === void 0 ? void 0 : _a.enforceHttps) !== false, certificatePinning: { enabled: ((_c = (_b = this.config.security) === null || _b === void 0 ? void 0 : _b.certificatePinning) === null || _c === void 0 ? void 0 : _c.enabled) || false, pins: ((_e = (_d = this.config.security) === null || _d === void 0 ? void 0 : _d.certificatePinning) === null || _e === void 0 ? void 0 : _e.pins) || [], }, validateInputs: ((_f = this.config.security) === null || _f === void 0 ? void 0 : _f.validateInputs) !== false, secureStorage: ((_g = this.config.security) === null || _g === void 0 ? void 0 : _g.secureStorage) !== false, }; } /** * Live Update Methods */ async sync(_options) { // console.log('Web: Checking for updates...', options); var _a; try { // In web, we can check for service worker updates if ('serviceWorker' in navigator) { const registration = await navigator.serviceWorker.getRegistration(); if (registration) { await registration.update(); if (registration.waiting) { // There's an update waiting registration.waiting.postMessage({ type: 'SKIP_WAITING' }); return { status: SyncStatus.UPDATE_AVAILABLE, version: 'web-update', description: 'Service worker update available', }; } } } return { status: SyncStatus.UP_TO_DATE, version: ((_a = this.currentBundle) === null || _a === void 0 ? void 0 : _a.version) || '1.0.0', }; } catch (error) { return { status: SyncStatus.ERROR, error: this.createError(UpdateErrorCode.UNKNOWN_ERROR, error.message), }; } } async download(options) { var _a; // Validate URL if (!options.url.startsWith('https://') && ((_a = this.config.security) === null || _a === void 0 ? void 0 : _a.enforceHttps) !== false) { throw this.createError(UpdateErrorCode.INSECURE_URL, 'Download URL must use HTTPS'); } const bundleId = `bundle-${Date.now()}`; const bundle = { bundleId, version: options.version, path: options.url, downloadTime: Date.now(), size: 0, // Would be calculated during actual download status: BundleStatus.DOWNLOADING, checksum: options.checksum, signature: options.signature, verified: false, }; this.bundles.set(bundleId, bundle); // Simulate download progress for (let i = 0; i <= 100; i += 10) { await this.notifyListeners('downloadProgress', { percent: i, bytesDownloaded: i * 1000, totalBytes: 10000, bundleId, }); await new Promise((resolve) => setTimeout(resolve, 100)); } // Update bundle status bundle.status = BundleStatus.READY; bundle.verified = true; // In real implementation, would verify checksum/signature this.saveStoredData(); await this.notifyListeners('updateStateChanged', { status: bundle.status, bundleId: bundle.bundleId, version: bundle.version, }); return bundle; } async set(bundle) { if (!this.bundles.has(bundle.bundleId)) { throw this.createError(UpdateErrorCode.UNKNOWN_ERROR, 'Bundle not found'); } const previousBundle = this.currentBundle; this.currentBundle = bundle; this.currentBundle.status = BundleStatus.ACTIVE; // Update previous bundle status if (previousBundle) { previousBundle.status = BundleStatus.READY; } this.saveStoredData(); } async reload() { // console.log('Web: Reloading application...'); // In web, we can reload the page if (typeof window !== 'undefined') { window.location.reload(); } } async reset() { // console.log('Web: Resetting to original bundle...'); this.currentBundle = null; this.bundles.clear(); this.saveStoredData(); // Clear service worker cache if available if ('caches' in window) { const cacheNames = await caches.keys(); await Promise.all(cacheNames.map((name) => caches.delete(name))); } } async current() { return this.currentBundle || this.createDefaultBundle(); } async list() { return Array.from(this.bundles.values()); } async delete(options) { if (options.bundleId) { this.bundles.delete(options.bundleId); } else if (options.keepVersions !== undefined) { const sortedBundles = Array.from(this.bundles.values()).sort((a, b) => b.downloadTime - a.downloadTime); const bundlesToDelete = sortedBundles.slice(options.keepVersions); bundlesToDelete.forEach((bundle) => this.bundles.delete(bundle.bundleId)); } else if (options.olderThan !== undefined) { const cutoffTime = options.olderThan; Array.from(this.bundles.values()) .filter((bundle) => bundle.downloadTime < cutoffTime) .forEach((bundle) => this.bundles.delete(bundle.bundleId)); } this.saveStoredData(); } async notifyAppReady() { // console.log('Web: App ready notification received'); if (this.currentBundle) { this.currentBundle.verified = true; this.saveStoredData(); } } async getLatest() { // In web, check for service worker updates if ('serviceWorker' in navigator) { const registration = await navigator.serviceWorker.getRegistration(); if (registration === null || registration === void 0 ? void 0 : registration.waiting) { return { available: true, version: 'web-update', notes: 'Service worker update available', }; } } return { available: false, }; } async setChannel(channel) { if (!this.config.liveUpdate) { this.config.liveUpdate = {}; } this.config.liveUpdate.channel = channel; this.saveConfiguration(); } async setUpdateUrl(url) { var _a; if (!url.startsWith('https://') && ((_a = this.config.security) === null || _a === void 0 ? void 0 : _a.enforceHttps) !== false) { throw this.createError(UpdateErrorCode.INSECURE_URL, 'Update URL must use HTTPS'); } if (!this.config.liveUpdate) { this.config.liveUpdate = {}; } this.config.liveUpdate.serverUrl = url; this.saveConfiguration(); } async validateUpdate(options) { // Simulate validation const checksumValid = await this.validateChecksum(options.bundlePath, options.checksum); const signatureValid = options.signature ? await this.validateSignature(options.bundlePath, options.signature) : true; const sizeValid = options.maxSize ? true : true; // Would check actual size const isValid = checksumValid && signatureValid && sizeValid; return { isValid, error: isValid ? undefined : 'Validation failed', details: { checksumValid, signatureValid, sizeValid, versionValid: true, }, }; } /** * App Update Methods */ async getAppUpdateInfo() { // console.log('Web: App updates not supported on web platform'); return { updateAvailable: false, currentVersion: '1.0.0', immediateUpdateAllowed: false, flexibleUpdateAllowed: false, }; } async performImmediateUpdate() { throw this.createError(UpdateErrorCode.PLATFORM_NOT_SUPPORTED, 'App updates are not supported on web platform'); } async startFlexibleUpdate() { throw this.createError(UpdateErrorCode.PLATFORM_NOT_SUPPORTED, 'App updates are not supported on web platform'); } async completeFlexibleUpdate() { throw this.createError(UpdateErrorCode.PLATFORM_NOT_SUPPORTED, 'App updates are not supported on web platform'); } async openAppStore(_options) { // console.log('Web: Opening app store fallback URL', options); var _a, _b, _c, _d; // Fallback to website or app landing page const fallbackUrl = ((_b = (_a = this.config.appUpdate) === null || _a === void 0 ? void 0 : _a.storeUrl) === null || _b === void 0 ? void 0 : _b.android) || ((_d = (_c = this.config.appUpdate) === null || _c === void 0 ? void 0 : _c.storeUrl) === null || _d === void 0 ? void 0 : _d.ios) || 'https://example.com/download'; window.open(fallbackUrl, '_blank'); } /** * App Review Methods */ async requestReview() { const canRequest = await this.canRequestReview(); if (!canRequest.canRequest) { return { displayed: false, error: canRequest.reason, }; } // console.log('Web: Showing review request fallback'); // Update last request time this.lastReviewRequest = Date.now(); localStorage.setItem('native-update-last-review', this.lastReviewRequest.toString()); // In web, we could show a custom modal or redirect to a review page const reviewUrl = 'https://example.com/review'; const shouldRedirect = confirm('Would you like to leave a review for our app?'); if (shouldRedirect) { window.open(reviewUrl, '_blank'); } return { displayed: true, }; } async canRequestReview() { const config = this.config.appReview; const now = Date.now(); const installDate = this.getInstallDate(); const daysSinceInstall = (now - installDate) / (1000 * 60 * 60 * 24); // Check minimum days since install if ((config === null || config === void 0 ? void 0 : config.minimumDaysSinceInstall) && daysSinceInstall < config.minimumDaysSinceInstall) { return { canRequest: false, reason: 'Not enough days since install', }; } // Check minimum days since last prompt if ((config === null || config === void 0 ? void 0 : config.minimumDaysSinceLastPrompt) && this.lastReviewRequest > 0) { const daysSinceLastPrompt = (now - this.lastReviewRequest) / (1000 * 60 * 60 * 24); if (daysSinceLastPrompt < config.minimumDaysSinceLastPrompt) { return { canRequest: false, reason: 'Too soon since last review request', }; } } // Check minimum launch count if ((config === null || config === void 0 ? void 0 : config.minimumLaunchCount) && this.launchCount < config.minimumLaunchCount) { return { canRequest: false, reason: 'Not enough app launches', }; } return { canRequest: true, }; } /** * Background Update Methods */ async enableBackgroundUpdates(config) { // console.log('Web: Enabling background updates', config); if (!this.config.backgroundUpdate) { this.config.backgroundUpdate = config; } else { this.config.backgroundUpdate = Object.assign(Object.assign({}, this.config.backgroundUpdate), config); } this.backgroundUpdateStatus.enabled = config.enabled; if (config.enabled) { await this.scheduleBackgroundCheck(config.checkInterval); } else { await this.disableBackgroundUpdates(); } this.saveConfiguration(); } async disableBackgroundUpdates() { // console.log('Web: Disabling background updates'); if (this.backgroundCheckInterval) { clearInterval(this.backgroundCheckInterval); this.backgroundCheckInterval = null; } this.backgroundUpdateStatus.enabled = false; this.backgroundUpdateStatus.isRunning = false; this.backgroundUpdateStatus.currentTaskId = undefined; if (this.config.backgroundUpdate) { this.config.backgroundUpdate.enabled = false; } this.saveConfiguration(); } async getBackgroundUpdateStatus() { return Object.assign({}, this.backgroundUpdateStatus); } async scheduleBackgroundCheck(interval) { // console.log('Web: Scheduling background check with interval', interval); if (this.backgroundCheckInterval) { clearInterval(this.backgroundCheckInterval); } this.backgroundCheckInterval = setInterval(async () => { if (this.backgroundUpdateStatus.enabled && !this.backgroundUpdateStatus.isRunning) { await this.triggerBackgroundCheck(); } }, interval); this.backgroundUpdateStatus.nextCheckTime = Date.now() + interval; } async triggerBackgroundCheck() { // console.log('Web: Triggering background check'); if (!this.backgroundUpdateStatus.enabled) { return { success: false, updatesFound: false, notificationSent: false, error: { code: UpdateErrorCode.INVALID_CONFIG, message: 'Background updates not enabled', }, }; } this.backgroundUpdateStatus.isRunning = true; this.backgroundUpdateStatus.checkCount++; this.backgroundUpdateStatus.lastCheckTime = Date.now(); try { const updateInfo = await this.getAppUpdateInfo(); const liveUpdate = await this.getLatest(); const updatesFound = updateInfo.updateAvailable || liveUpdate.available; let notificationSent = false; if (updatesFound) { notificationSent = await this.sendWebNotification(updateInfo, liveUpdate); } this.backgroundUpdateStatus.isRunning = false; this.backgroundUpdateStatus.lastUpdateTime = updatesFound ? Date.now() : undefined; this.backgroundUpdateStatus.lastError = undefined; return { success: true, updatesFound, appUpdate: updateInfo.updateAvailable ? updateInfo : undefined, liveUpdate: liveUpdate.available ? liveUpdate : undefined, notificationSent, }; } catch (error) { this.backgroundUpdateStatus.isRunning = false; this.backgroundUpdateStatus.failureCount++; this.backgroundUpdateStatus.lastError = { code: UpdateErrorCode.UNKNOWN_ERROR, message: error instanceof Error ? error.message : 'Unknown error', }; return { success: false, updatesFound: false, notificationSent: false, error: this.backgroundUpdateStatus.lastError, }; } } async setNotificationPreferences(preferences) { // console.log('Web: Setting notification preferences', preferences); if (!this.config.backgroundUpdate) { this.config.backgroundUpdate = {}; } this.config.backgroundUpdate.notificationPreferences = preferences; this.saveConfiguration(); } async getNotificationPermissions() { if (!('Notification' in window)) { return { granted: false, canRequest: false, }; } const permission = Notification.permission; return { granted: permission === 'granted', canRequest: permission === 'default', }; } async requestNotificationPermissions() { if (!('Notification' in window)) { return false; } if (Notification.permission === 'granted') { return true; } if (Notification.permission === 'denied') { return false; } const permission = await Notification.requestPermission(); return permission === 'granted'; } async sendWebNotification(appUpdate, liveUpdate) { var _a; const permissions = await this.getNotificationPermissions(); if (!permissions.granted) { return false; } const prefs = (_a = this.config.backgroundUpdate) === null || _a === void 0 ? void 0 : _a.notificationPreferences; let title = (prefs === null || prefs === void 0 ? void 0 : prefs.title) || 'App Update Available'; let body = (prefs === null || prefs === void 0 ? void 0 : prefs.description) || 'A new version of the app is available'; if (appUpdate.updateAvailable && liveUpdate.available) { title = 'App Updates Available'; body = `App version ${appUpdate.availableVersion} and content updates are available`; } else if (appUpdate.updateAvailable) { title = 'App Update Available'; body = `Version ${appUpdate.availableVersion} is available`; } else if (liveUpdate.available) { title = 'Content Update Available'; body = `New content version ${liveUpdate.version} is available`; } try { const notification = new Notification(title, { body, icon: (prefs === null || prefs === void 0 ? void 0 : prefs.iconName) || '/favicon.ico', badge: '/favicon.ico', silent: !(prefs === null || prefs === void 0 ? void 0 : prefs.soundEnabled), data: { type: 'update_available', appUpdate, liveUpdate, }, }); notification.onclick = () => { window.focus(); notification.close(); this.notifyListeners('backgroundUpdateNotification', { type: appUpdate.updateAvailable ? 'app_update' : 'live_update', updateAvailable: true, version: appUpdate.availableVersion || liveUpdate.version, action: 'tapped', }); }; return true; } catch (error) { console.error('Web: Failed to send notification', error); return false; } } /** * Helper Methods */ createError(code, message) { return { code, message, }; } createDefaultBundle() { return { bundleId: 'default', version: '1.0.0', path: '/', downloadTime: Date.now(), size: 0, status: BundleStatus.ACTIVE, checksum: '', verified: true, }; } async validateChecksum(_data, _expectedChecksum) { // In a real implementation, would calculate actual checksum // console.log('Web: Validating checksum...', expectedChecksum); return true; } async validateSignature(_data, _signature) { // In a real implementation, would verify signature // console.log('Web: Validating signature...', signature); return true; } loadStoredData() { // Load configuration const storedConfig = localStorage.getItem('native-update-config'); if (storedConfig) { this.config = JSON.parse(storedConfig); } // Load bundles const storedBundles = localStorage.getItem('native-update-bundles'); if (storedBundles) { const bundlesArray = JSON.parse(storedBundles); bundlesArray.forEach((bundle) => this.bundles.set(bundle.bundleId, bundle)); } // Load current bundle const storedCurrent = localStorage.getItem('native-update-current'); if (storedCurrent) { this.currentBundle = JSON.parse(storedCurrent); } // Load last review request time const storedLastReview = localStorage.getItem('native-update-last-review'); if (storedLastReview) { this.lastReviewRequest = parseInt(storedLastReview, 10); } // Load launch count const storedLaunchCount = localStorage.getItem('native-update-launch-count'); if (storedLaunchCount) { this.launchCount = parseInt(storedLaunchCount, 10); } } saveStoredData() { localStorage.setItem('native-update-bundles', JSON.stringify(Array.from(this.bundles.values()))); if (this.currentBundle) { localStorage.setItem('native-update-current', JSON.stringify(this.currentBundle)); } } saveConfiguration() { localStorage.setItem('native-update-config', JSON.stringify(this.config)); } getInstallDate() { const stored = localStorage.getItem('native-update-install-date'); if (stored) { return parseInt(stored, 10); } const now = Date.now(); localStorage.setItem('native-update-install-date', now.toString()); return now; } incrementLaunchCount() { this.launchCount++; localStorage.setItem('native-update-launch-count', this.launchCount.toString()); } } // Alias for backward compatibility with tests // Already exported above //# sourceMappingURL=web.js.map