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