native-update 1.3.3 → 1.3.5

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/Readme.md CHANGED
@@ -4,7 +4,7 @@
4
4
  >
5
5
  > This package is now **feature-complete** with significant improvements:
6
6
  >
7
- > - ✅ **pnpm Workspace Monorepo** - Seamless development with workspace:* references
7
+ > - ✅ **yarn Workspace Monorepo** - Seamless development with workspace:* references
8
8
  > - ✅ **3 Complete Examples** - React+Capacitor frontend, Node.js+Express and Firebase backends in `example-apps/`
9
9
  > - ✅ **Native Implementations Complete** - iOS (Swift) and Android (Kotlin) fully implemented
10
10
  > - ✅ **Comprehensive Test Suite** - Unit and integration tests with Vitest
@@ -290,7 +290,7 @@ This plugin implements multiple security layers:
290
290
 
291
291
  ## 🎯 Complete Example Implementations
292
292
 
293
- This repository uses **pnpm workspace** for seamless development. All examples reference the local plugin via `workspace:*` - no need to publish to npm for testing!
293
+ This repository uses **yarn workspace** for seamless development. All examples reference the local plugin via `workspace:*` - no need to publish to npm for testing!
294
294
 
295
295
  ### Frontend Example: React + Capacitor
296
296
 
@@ -58,6 +58,13 @@ android {
58
58
  kotlinOptions {
59
59
  jvmTarget = '17'
60
60
  }
61
+
62
+ testOptions {
63
+ unitTests {
64
+ includeAndroidResources = true
65
+ returnDefaultValues = true
66
+ }
67
+ }
61
68
  }
62
69
 
63
70
  repositories {
@@ -97,6 +104,11 @@ dependencies {
97
104
 
98
105
  // Testing
99
106
  testImplementation 'junit:junit:4.13.2'
107
+ testImplementation 'org.mockito:mockito-core:5.8.0'
108
+ testImplementation 'org.mockito.kotlin:mockito-kotlin:5.2.1'
109
+ testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
110
+ testImplementation 'com.google.truth:truth:1.1.5'
111
+ testImplementation 'org.robolectric:robolectric:4.11.1'
100
112
  androidTestImplementation 'androidx.test.ext:junit:1.1.5'
101
113
  androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
102
114
  }
package/dist/plugin.js CHANGED
@@ -1,3 +1,3 @@
1
- /*! Native Update Plugin v1.1.6 | MIT License */
1
+ /*! Native Update Plugin v1.3.5 | MIT License */
2
2
  !function(e,t,r,a){var i,n,s,o,l,c,d,h,u,g,p,f;!function(e){e.APP_UPDATE="app_update",e.LIVE_UPDATE="live_update",e.BOTH="both"}(i||(i={})),function(e){e.MIN="min",e.LOW="low",e.DEFAULT="default",e.HIGH="high",e.MAX="max"}(n||(n={})),function(e){e.IMMEDIATE="immediate",e.BACKGROUND="background",e.MANUAL="manual"}(s||(s={})),function(e){e.IMMEDIATE="immediate",e.ON_NEXT_RESTART="on_next_restart",e.ON_NEXT_RESUME="on_next_resume"}(o||(o={})),function(e){e.IMMEDIATE="immediate",e.ON_NEXT_RESTART="on_next_restart",e.ON_NEXT_RESUME="on_next_resume"}(l||(l={})),function(e){e.SHA256="SHA-256",e.SHA512="SHA-512"}(c||(c={})),function(e){e.UP_TO_DATE="UP_TO_DATE",e.UPDATE_AVAILABLE="UPDATE_AVAILABLE",e.UPDATE_INSTALLED="UPDATE_INSTALLED",e.ERROR="ERROR"}(d||(d={})),function(e){e.PENDING="PENDING",e.DOWNLOADING="DOWNLOADING",e.READY="READY",e.ACTIVE="ACTIVE",e.FAILED="FAILED"}(h||(h={})),function(e){e.UNKNOWN="UNKNOWN",e.PENDING="PENDING",e.DOWNLOADING="DOWNLOADING",e.DOWNLOADED="DOWNLOADED",e.INSTALLING="INSTALLING",e.INSTALLED="INSTALLED",e.FAILED="FAILED",e.CANCELED="CANCELED"}(u||(u={})),function(e){e.NETWORK_ERROR="NETWORK_ERROR",e.SERVER_ERROR="SERVER_ERROR",e.TIMEOUT_ERROR="TIMEOUT_ERROR",e.DOWNLOAD_ERROR="DOWNLOAD_ERROR",e.STORAGE_ERROR="STORAGE_ERROR",e.SIZE_LIMIT_EXCEEDED="SIZE_LIMIT_EXCEEDED",e.VERIFICATION_ERROR="VERIFICATION_ERROR",e.CHECKSUM_ERROR="CHECKSUM_ERROR",e.SIGNATURE_ERROR="SIGNATURE_ERROR",e.INSECURE_URL="INSECURE_URL",e.INVALID_CERTIFICATE="INVALID_CERTIFICATE",e.PATH_TRAVERSAL="PATH_TRAVERSAL",e.INSTALL_ERROR="INSTALL_ERROR",e.ROLLBACK_ERROR="ROLLBACK_ERROR",e.VERSION_MISMATCH="VERSION_MISMATCH",e.PERMISSION_DENIED="PERMISSION_DENIED",e.UPDATE_NOT_AVAILABLE="UPDATE_NOT_AVAILABLE",e.UPDATE_IN_PROGRESS="UPDATE_IN_PROGRESS",e.UPDATE_CANCELLED="UPDATE_CANCELLED",e.PLATFORM_NOT_SUPPORTED="PLATFORM_NOT_SUPPORTED",e.REVIEW_NOT_SUPPORTED="REVIEW_NOT_SUPPORTED",e.QUOTA_EXCEEDED="QUOTA_EXCEEDED",e.CONDITIONS_NOT_MET="CONDITIONS_NOT_MET",e.INVALID_CONFIG="INVALID_CONFIG",e.UNKNOWN_ERROR="UNKNOWN_ERROR"}(g||(g={}));class m{constructor(){this.config=this.getDefaultConfig()}static getInstance(){return m.instance||(m.instance=new m),m.instance}getDefaultConfig(){return{filesystem:null,preferences:null,baseUrl:"",allowedHosts:[],maxBundleSize:104857600,downloadTimeout:3e4,retryAttempts:3,retryDelay:1e3,enableSignatureValidation:!0,publicKey:"",cacheExpiration:864e5,enableLogging:!1,serverUrl:"",channel:"production",appId:"",autoCheck:!0,autoUpdate:!1,updateStrategy:"background",requireSignature:!0,checksumAlgorithm:"SHA-256",checkInterval:864e5,security:{enforceHttps:!0,validateInputs:!0,secureStorage:!0,logSecurityEvents:!1},promptAfterPositiveEvents:!1,maxPromptsPerVersion:1,minimumDaysSinceLastPrompt:7,isPremiumUser:!1,appStoreId:"",iosAppId:"",packageName:"",webReviewUrl:"",minimumVersion:"1.0.0",backendType:"http",firestore:null,enableDeltaUpdates:!0,enableStagedRollouts:!0}}configure(e){this.config=Object.assign(Object.assign({},this.config),e),this.validateConfig()}validateConfig(){if(this.config.maxBundleSize<=0)throw new Error("maxBundleSize must be greater than 0");if(this.config.downloadTimeout<=0)throw new Error("downloadTimeout must be greater than 0");if(this.config.retryAttempts<0)throw new Error("retryAttempts must be non-negative");if(this.config.retryDelay<0)throw new Error("retryDelay must be non-negative")}get(e){return this.config[e]}set(e,t){this.config[e]=t}getAll(){return Object.assign({},this.config)}isConfigured(){return!(!this.config.filesystem||!this.config.preferences)}}e.LogLevel=void 0,(p=e.LogLevel||(e.LogLevel={}))[p.DEBUG=0]="DEBUG",p[p.INFO=1]="INFO",p[p.WARN=2]="WARN",p[p.ERROR=3]="ERROR";class w{constructor(e){this.configManager=m.getInstance(),this.context=e||"NativeUpdate"}static getInstance(){return w.instance||(w.instance=new w),w.instance}shouldLog(){return this.configManager.get("enableLogging")}sanitize(e){if("string"==typeof e){let t=e;return t=t.replace(/\/[^\s]+\/([\w.-]+)$/g,"/<path>/$1"),t=t.replace(/https?:\/\/[^:]+:[^@]+@/g,"https://***:***@"),t=t.replace(/[a-zA-Z0-9]{32,}/g,"<redacted>"),t}if("object"==typeof e&&null!==e){if(Array.isArray(e))return e.map(e=>this.sanitize(e));{const t={},r=e;for(const e in r)t[e]=e.toLowerCase().includes("key")||e.toLowerCase().includes("secret")||e.toLowerCase().includes("password")||e.toLowerCase().includes("token")?"<redacted>":this.sanitize(r[e]);return t}}return e}log(t,r){this.logWithLevel(e.LogLevel.INFO,t,r)}logWithLevel(t,r,a){if(!this.shouldLog())return;const i=(new Date).toISOString(),n=a?this.sanitize(a):void 0,s={timestamp:i,level:e.LogLevel[t],context:this.context,message:r};switch(void 0!==n&&(s.data=n),t){case e.LogLevel.DEBUG:console.debug(`[${this.context}]`,s);break;case e.LogLevel.INFO:console.info(`[${this.context}]`,s);break;case e.LogLevel.WARN:console.warn(`[${this.context}]`,s);break;case e.LogLevel.ERROR:console.error(`[${this.context}]`,s)}}debug(t,r){this.logWithLevel(e.LogLevel.DEBUG,t,r)}info(t,r){this.logWithLevel(e.LogLevel.INFO,t,r)}warn(t,r){this.logWithLevel(e.LogLevel.WARN,t,r)}error(t,r){const a=r instanceof Error?{name:r.name,message:r.message,stack:r.stack}:r;this.logWithLevel(e.LogLevel.ERROR,t,a)}}e.ErrorCode=void 0,(f=e.ErrorCode||(e.ErrorCode={})).NOT_CONFIGURED="NOT_CONFIGURED",f.INVALID_CONFIG="INVALID_CONFIG",f.MISSING_DEPENDENCY="MISSING_DEPENDENCY",f.DOWNLOAD_FAILED="DOWNLOAD_FAILED",f.DOWNLOAD_TIMEOUT="DOWNLOAD_TIMEOUT",f.INVALID_URL="INVALID_URL",f.UNAUTHORIZED_HOST="UNAUTHORIZED_HOST",f.BUNDLE_TOO_LARGE="BUNDLE_TOO_LARGE",f.CHECKSUM_MISMATCH="CHECKSUM_MISMATCH",f.SIGNATURE_INVALID="SIGNATURE_INVALID",f.VERSION_DOWNGRADE="VERSION_DOWNGRADE",f.INVALID_BUNDLE_FORMAT="INVALID_BUNDLE_FORMAT",f.STORAGE_FULL="STORAGE_FULL",f.FILE_NOT_FOUND="FILE_NOT_FOUND",f.PERMISSION_DENIED="PERMISSION_DENIED",f.UPDATE_FAILED="UPDATE_FAILED",f.ROLLBACK_FAILED="ROLLBACK_FAILED",f.BUNDLE_NOT_READY="BUNDLE_NOT_READY",f.PLATFORM_NOT_SUPPORTED="PLATFORM_NOT_SUPPORTED",f.NATIVE_ERROR="NATIVE_ERROR";class E extends Error{constructor(e,t,r,a){super(t),this.code=e,this.message=t,this.details=r,this.originalError=a,this.name="NativeUpdateError",Object.setPrototypeOf(this,E.prototype)}toJSON(){return{name:this.name,code:this.code,message:this.message,details:this.details,stack:this.stack}}}class I extends E{constructor(e,t,r,a){super(e,t,r,a),this.name="DownloadError"}}class y extends E{constructor(e,t,r){super(e,t,r),this.name="ValidationError"}}class A extends E{constructor(e,t,r,a){super(e,t,r,a),this.name="StorageError"}}class D extends E{constructor(e,t,r,a){super(e,t,r,a),this.name="UpdateError"}}class v{constructor(){this.configManager=m.getInstance(),this.logger=w.getInstance()}static getInstance(){return v.instance||(v.instance=new v),v.instance}static validateUrl(e){try{return"https:"===new URL(e).protocol}catch(e){return!1}}static validateChecksum(e){return/^[a-f0-9]{64}$/i.test(e)}static sanitizeInput(e){return e?e.replace(/<[^>]*>/g,"").replace(/[^\w\s/.-]/g,""):""}static validateBundleSize(e){return e>0&&e<=104857600}async calculateChecksum(e){const t=await crypto.subtle.digest("SHA-256",e);return Array.from(new Uint8Array(t)).map(e=>e.toString(16).padStart(2,"0")).join("")}async verifyChecksum(e,t){if(!t)return this.logger.warn("No checksum provided for verification"),!0;const r=await this.calculateChecksum(e),a=r===t.toLowerCase();return a||this.logger.error("Checksum verification failed",{expected:t,actual:r}),a}async validateChecksum(e,t){return this.verifyChecksum(e,t)}async verifySignature(t,r){if(!this.configManager.get("enableSignatureValidation"))return!0;const a=this.configManager.get("publicKey");if(!a)throw new y(e.ErrorCode.SIGNATURE_INVALID,"Public key not configured for signature validation");try{const e=await crypto.subtle.importKey("spki",this.pemToArrayBuffer(a),{name:"RSA-PSS",hash:"SHA-256"},!1,["verify"]),i=await crypto.subtle.verify({name:"RSA-PSS",saltLength:32},e,this.base64ToArrayBuffer(r),t);return i||this.logger.error("Signature verification failed"),i}catch(e){return this.logger.error("Signature verification error",e),!1}}pemToArrayBuffer(e){const t=e.replace(/-----BEGIN PUBLIC KEY-----/g,"").replace(/-----END PUBLIC KEY-----/g,"").replace(/\s/g,"");return this.base64ToArrayBuffer(t)}base64ToArrayBuffer(e){const t=atob(e),r=new Uint8Array(t.length);for(let e=0;e<t.length;e++)r[e]=t.charCodeAt(e);return r.buffer}sanitizePath(e){return e.split("/").filter(e=>".."!==e&&"."!==e).join("/").replace(/^\/+/,"")}validateBundleId(t){if(!t||"string"!=typeof t)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID must be a non-empty string");if(!/^[a-zA-Z0-9\-_.]+$/.test(t))throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID contains invalid characters");if(t.length>100)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID is too long (max 100 characters)")}validateVersion(t){if(!t||"string"!=typeof t)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Version must be a non-empty string");if(!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/.test(t))throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Version must follow semantic versioning format (e.g., 1.2.3)")}isVersionDowngrade(e,t){const r=this.parseVersion(e),a=this.parseVersion(t);return a.major<r.major||!(a.major>r.major)&&(a.minor<r.minor||!(a.minor>r.minor)&&a.patch<r.patch)}parseVersion(e){const t=e.split("-")[0].split(".");return{major:parseInt(t[0],10)||0,minor:parseInt(t[1],10)||0,patch:parseInt(t[2],10)||0}}validateUrl(t){if(!t||"string"!=typeof t)throw new y(e.ErrorCode.INVALID_URL,"URL must be a non-empty string");let r;try{r=new URL(t)}catch(t){throw new y(e.ErrorCode.INVALID_URL,"Invalid URL format")}if("https:"!==r.protocol)throw new y(e.ErrorCode.INVALID_URL,"Only HTTPS URLs are allowed");const a=this.configManager.get("allowedHosts");if(a.length>0&&!a.includes(r.hostname))throw new y(e.ErrorCode.UNAUTHORIZED_HOST,`Host ${r.hostname} is not in the allowed hosts list`);if([/^localhost$/i,/^127\./,/^10\./,/^172\.(1[6-9]|2[0-9]|3[0-1])\./,/^192\.168\./,/^::1$/,/^fc00:/i,/^fe80:/i].some(e=>e.test(r.hostname)))throw new y(e.ErrorCode.UNAUTHORIZED_HOST,"Private/local addresses are not allowed")}validateFileSize(t){if("number"!=typeof t||t<0)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"File size must be a non-negative number");const r=this.configManager.get("maxBundleSize");if(t>r)throw new y(e.ErrorCode.BUNDLE_TOO_LARGE,`File size ${t} exceeds maximum allowed size of ${r} bytes`)}generateSecureId(){const e=new Uint8Array(16);return crypto.getRandomValues(e),Array.from(e,e=>e.toString(16).padStart(2,"0")).join("")}async validateCertificatePin(e,t){const r=this.configManager.certificatePins;if(!r||!Array.isArray(r)||0===r.length)return!0;const a=r.filter(t=>t.hostname===e);if(0===a.length)return!0;const i=await this.calculateCertificateHash(t),n=a.some(e=>e.sha256===i);return n||this.logger.error("Certificate pinning validation failed",{hostname:e,expectedPins:a.map(e=>e.sha256),actualHash:i}),n}async calculateCertificateHash(e){const t=(new TextEncoder).encode(e),r=await crypto.subtle.digest("SHA-256",t),a=Array.from(new Uint8Array(r));return"sha256/"+btoa(String.fromCharCode(...a))}validateMetadata(t){if(t&&"object"!=typeof t)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Metadata must be an object");if(JSON.stringify(t||{}).length>10240)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Metadata is too large (max 10KB)")}}class N{constructor(){this.STORAGE_KEY="capacitor_native_update_bundles",this.ACTIVE_BUNDLE_KEY="capacitor_native_update_active",this.preferences=null,this.cache=new Map,this.cacheExpiry=0,this.logger=w.getInstance(),this.configManager=m.getInstance()}async initialize(){if(this.preferences=this.configManager.get("preferences"),!this.preferences)throw new A(e.ErrorCode.MISSING_DEPENDENCY,"Preferences not configured. Please configure the plugin first.");await this.loadCache()}async loadCache(){if(!(Date.now()<this.cacheExpiry))try{const{value:e}=await this.preferences.get({key:this.STORAGE_KEY});if(e){const t=JSON.parse(e);this.cache.clear(),t.forEach(e=>this.cache.set(e.bundleId,e))}this.cacheExpiry=Date.now()+5e3}catch(e){this.logger.error("Failed to load bundles from storage",e),this.cache.clear()}}async saveCache(){try{const e=Array.from(this.cache.values());await this.preferences.set({key:this.STORAGE_KEY,value:JSON.stringify(e)}),this.logger.debug("Saved bundles to storage",{count:e.length})}catch(t){throw new A(e.ErrorCode.STORAGE_FULL,"Failed to save bundles to storage",void 0,t)}}async saveBundleInfo(e){this.validateBundleInfo(e),this.cache.set(e.bundleId,e),await this.saveCache(),this.logger.info("Bundle saved",{bundleId:e.bundleId,version:e.version})}validateBundleInfo(t){if(!t.bundleId||"string"!=typeof t.bundleId)throw new A(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Invalid bundle ID");if(!t.version||"string"!=typeof t.version)throw new A(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Invalid bundle version");if(!t.path||"string"!=typeof t.path)throw new A(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Invalid bundle path");if("number"!=typeof t.size||t.size<0)throw new A(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Invalid bundle size")}async getAllBundles(){return await this.loadCache(),Array.from(this.cache.values())}async getBundle(t){if(!t)throw new A(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID is required");return await this.loadCache(),this.cache.get(t)||null}async deleteBundle(t){if(!t)throw new A(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID is required");await this.loadCache(),this.cache.get(t)?(this.cache.delete(t),await this.saveCache(),await this.getActiveBundleId()===t&&await this.clearActiveBundle(),this.logger.info("Bundle deleted",{bundleId:t})):this.logger.warn("Attempted to delete non-existent bundle",{bundleId:t})}async getActiveBundle(){const e=await this.getActiveBundleId();return e?this.getBundle(e):null}async setActiveBundle(t){if(!t)throw new A(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID is required");const r=await this.getBundle(t);if(!r)throw new A(e.ErrorCode.FILE_NOT_FOUND,`Bundle ${t} not found`);const a=await this.getActiveBundle();a&&a.bundleId!==t&&(a.status="READY",await this.saveBundleInfo(a)),r.status="ACTIVE",await this.saveBundleInfo(r),await this.preferences.set({key:this.ACTIVE_BUNDLE_KEY,value:t}),this.logger.info("Active bundle set",{bundleId:t,version:r.version})}async getActiveBundleId(){try{const{value:e}=await this.preferences.get({key:this.ACTIVE_BUNDLE_KEY});return e}catch(e){return this.logger.error("Failed to get active bundle ID",e),null}}async clearActiveBundle(){await this.preferences.remove({key:this.ACTIVE_BUNDLE_KEY}),this.logger.info("Active bundle cleared")}async clearAllBundles(){await this.preferences.remove({key:this.STORAGE_KEY}),await this.preferences.remove({key:this.ACTIVE_BUNDLE_KEY}),this.cache.clear(),this.cacheExpiry=0,this.logger.info("All bundles cleared")}async cleanupOldBundles(t){if(t<1)throw new A(e.ErrorCode.INVALID_CONFIG,"Keep count must be at least 1");const r=await this.getAllBundles(),a=await this.getActiveBundleId(),i=r.sort((e,t)=>t.downloadTime-e.downloadTime),n=new Set;a&&n.add(a);let s=n.size;for(const e of i){if(s>=t)break;n.has(e.bundleId)||(n.add(e.bundleId),s++)}let o=0;for(const e of r)n.has(e.bundleId)||(await this.deleteBundle(e.bundleId),o++);o>0&&this.logger.info("Cleaned up old bundles",{deleted:o,kept:s})}async getBundlesOlderThan(t){if(t<0)throw new A(e.ErrorCode.INVALID_CONFIG,"Timestamp must be non-negative");return(await this.getAllBundles()).filter(e=>e.downloadTime<t)}async markBundleAsVerified(t){const r=await this.getBundle(t);if(!r)throw new A(e.ErrorCode.FILE_NOT_FOUND,`Bundle ${t} not found`);r.verified=!0,await this.saveBundleInfo(r),this.logger.info("Bundle marked as verified",{bundleId:t})}async getTotalStorageUsed(){return(await this.getAllBundles()).reduce((e,t)=>e+t.size,0)}async isStorageLimitExceeded(e=0){const t=await this.getTotalStorageUsed();let r=3*this.configManager.get("maxBundleSize");try{if("storage"in navigator&&"estimate"in navigator.storage){const e=await navigator.storage.estimate();e.quota&&(r=Math.max(r,e.quota-104857600))}}catch(e){this.logger.warn("Storage API not available for quota check, using config limit")}return t+e>r}createDefaultBundle(){return{bundleId:"default",version:"1.0.0",path:"/",downloadTime:Date.now(),size:0,status:"ACTIVE",checksum:"",verified:!0}}async cleanExpiredBundles(){const e=this.configManager.get("cacheExpiration"),t=Date.now()-e,r=await this.getBundlesOlderThan(t);for(const e of r){const t=await this.getActiveBundleId();e.bundleId!==t&&await this.deleteBundle(e.bundleId)}}}class b{constructor(){this.activeDownloads=new Map,this.filesystem=null,this.logger=w.getInstance(),this.configManager=m.getInstance()}async initialize(){if(this.filesystem=this.configManager.get("filesystem"),!this.filesystem)throw new I(e.ErrorCode.MISSING_DEPENDENCY,"Filesystem not configured. Please configure the plugin first.")}validateUrl(t){try{const r=new URL(t);if("https:"!==r.protocol)throw new y(e.ErrorCode.INVALID_URL,"Only HTTPS URLs are allowed for security reasons");const a=this.configManager.get("allowedHosts");if(a.length>0&&!a.includes(r.hostname))throw new y(e.ErrorCode.UNAUTHORIZED_HOST,`Host ${r.hostname} is not in the allowed hosts list`)}catch(t){if(t instanceof y)throw t;throw new y(e.ErrorCode.INVALID_URL,"Invalid URL format")}}async download(t,r,a){if(this.validateUrl(t),!r)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID is required");if(this.activeDownloads.has(r))throw new I(e.ErrorCode.DOWNLOAD_FAILED,`Download already in progress for bundle ${r}`);const i=new AbortController,n={controller:i,startTime:Date.now()};this.activeDownloads.set(r,n);try{const s=this.configManager.get("downloadTimeout"),o=setTimeout(()=>i.abort(),s),l={"Cache-Control":"no-cache",Accept:"application/octet-stream, application/zip"};n.resumePosition&&n.resumePosition>0&&(l.Range=`bytes=${n.resumePosition}-`);const c=await fetch(t,{signal:i.signal,headers:l});if(clearTimeout(o),!c.ok)throw new I(e.ErrorCode.DOWNLOAD_FAILED,`Download failed: ${c.status} ${c.statusText}`,{status:c.status,statusText:c.statusText});const d=c.headers.get("content-type");if(d&&!this.isValidContentType(d))throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,`Invalid content type: ${d}`);const h=c.headers.get("content-length"),u=h?parseInt(h,10):0;if(u>this.configManager.get("maxBundleSize"))throw new y(e.ErrorCode.BUNDLE_TOO_LARGE,`Bundle size ${u} exceeds maximum allowed size`);if(!u||!c.body){const e=await c.blob();return this.validateBlobSize(e),e}const g=c.body.getReader(),p=[];let f=0;for(;;){const{done:t,value:i}=await g.read();if(t)break;if(p.push(i),f+=i.length,f>this.configManager.get("maxBundleSize"))throw new y(e.ErrorCode.BUNDLE_TOO_LARGE,"Download size exceeds maximum allowed size");if(a){const e=n.resumePosition||0,t=e+f,i=e+u;a({percent:Math.round(t/i*100),bytesDownloaded:t,totalBytes:i,bundleId:r})}}const m=new Blob(p);return this.validateBlobSize(m),this.logger.info("Download completed",{bundleId:r,size:m.size,duration:Date.now()-n.startTime}),m}catch(t){if(t instanceof Error&&"AbortError"===t.name){const r=Date.now()-n.startTime>=this.configManager.get("downloadTimeout");throw new I(r?e.ErrorCode.DOWNLOAD_TIMEOUT:e.ErrorCode.DOWNLOAD_FAILED,r?"Download timed out":"Download cancelled",void 0,t)}throw t}finally{this.activeDownloads.delete(r)}}async resumeDownload(e,t,r,a){const i=this.activeDownloads.get(t);i&&(i.resumePosition=r.size);try{const i=await this.download(e,t,a);return new Blob([r,i])}catch(e){throw this.activeDownloads.delete(t),e}}async canResume(e){try{return"bytes"===(await fetch(e,{method:"HEAD"})).headers.get("Accept-Ranges")}catch(e){return!1}}isValidContentType(e){return["application/octet-stream","application/zip","application/x-zip-compressed","application/x-zip"].some(t=>e.includes(t))}validateBlobSize(t){if(0===t.size)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Downloaded file is empty");if(t.size>this.configManager.get("maxBundleSize"))throw new y(e.ErrorCode.BUNDLE_TOO_LARGE,`File size ${t.size} exceeds maximum allowed size`)}cancelDownload(e){const t=this.activeDownloads.get(e);t&&(t.controller.abort(),this.activeDownloads.delete(e),this.logger.info("Download cancelled",{bundleId:e}))}cancelAllDownloads(){for(const e of this.activeDownloads.values())e.controller.abort();const e=this.activeDownloads.size;this.activeDownloads.clear(),e>0&&this.logger.info("All downloads cancelled",{count:e})}isDownloading(e){return this.activeDownloads.has(e)}getActiveDownloadCount(){return this.activeDownloads.size}async downloadWithRetry(t,r,a){const i=this.configManager.get("retryAttempts"),n=this.configManager.get("retryDelay");let s=null;for(let e=0;e<i;e++)try{if(e>0){const t=Math.min(n*Math.pow(2,e-1),3e4);await new Promise(e=>setTimeout(e,t)),this.logger.debug("Retrying download",{bundleId:r,attempt:e,delay:t})}return await this.download(t,r,a)}catch(t){if(s=t,t instanceof y||t instanceof Error&&"AbortError"===t.name)throw t;this.logger.warn(`Download attempt ${e+1} failed`,{bundleId:r,error:t})}throw new I(e.ErrorCode.DOWNLOAD_FAILED,"Download failed after all retries",{attempts:i},s||void 0)}async blobToArrayBuffer(e){return e.arrayBuffer()}async saveBlob(t,a){if(!this.filesystem)throw new I(e.ErrorCode.MISSING_DEPENDENCY,"Filesystem not initialized");const i=await this.blobToArrayBuffer(a),n=btoa(String.fromCharCode(...new Uint8Array(i))),s=`bundles/${t}/bundle.zip`;return await this.filesystem.writeFile({path:s,data:n,directory:r.Directory.Data,recursive:!0}),this.logger.debug("Bundle saved to filesystem",{bundleId:t,path:s,size:a.size}),s}async loadBlob(t){if(!this.filesystem)throw new I(e.ErrorCode.MISSING_DEPENDENCY,"Filesystem not initialized");try{const e=`bundles/${t}/bundle.zip`,a=await this.filesystem.readFile({path:e,directory:r.Directory.Data}),i=atob(a.data),n=new Uint8Array(i.length);for(let e=0;e<i.length;e++)n[e]=i.charCodeAt(e);return new Blob([n],{type:"application/zip"})}catch(e){return this.logger.debug("Failed to load bundle from filesystem",{bundleId:t,error:e}),null}}async deleteBlob(t){if(!this.filesystem)throw new I(e.ErrorCode.MISSING_DEPENDENCY,"Filesystem not initialized");try{const e=`bundles/${t}`;await this.filesystem.rmdir({path:e,directory:r.Directory.Data,recursive:!0}),this.logger.debug("Bundle deleted from filesystem",{bundleId:t})}catch(e){this.logger.warn("Failed to delete bundle from filesystem",{bundleId:t,error:e})}}}function C(e){return new Date(1e3*e.seconds+e.nanoseconds/1e6)}const U="manifests";function L(e,t){return`${e}_${t}`}class _{constructor(e){var t;this.cache=new Map,this.config={projectId:e.projectId,databaseId:e.databaseId||"(default)",appId:e.appId,channel:e.channel,cacheDuration:e.cacheDuration||3e5,enableOffline:null===(t=e.enableOffline)||void 0===t||t},this.logger=w.getInstance()}static getInstance(e){if(!_.instance&&e&&(_.instance=new _(e)),!_.instance)throw new Error("FirestoreClient not initialized. Call with config first.");return _.instance}static resetInstance(){_.instance=null}getBaseUrl(){return`https://firestore.googleapis.com/v1/projects/${this.config.projectId}/databases/${this.config.databaseId}/documents`}async getDocument(e,t){const r=`${e}/${t}`,a=this.getFromCache(r);if(null!==a)return this.logger.debug("Firestore cache hit",{collection:e,documentId:t}),a;const i=`${this.getBaseUrl()}/${e}/${t}`;try{this.logger.debug("Fetching Firestore document",{url:i});const a=await fetch(i,{method:"GET",headers:{"Content-Type":"application/json"}});if(404===a.status)return this.logger.debug("Firestore document not found",{collection:e,documentId:t}),null;if(!a.ok)throw new Error(`Firestore error: ${a.status} ${a.statusText}`);const n=await a.json(),s=this.parseDocument(n);return this.setCache(r,s),s}catch(e){this.logger.error("Firestore fetch error",e);const t=this.getFromCache(r,!0);if(null!==t)return this.logger.warn("Using stale cache due to fetch error"),t;throw e}}async getManifest(){const e=L(this.config.appId,this.config.channel);return this.getDocument(U,e)}async getManifestFor(e,t){const r=L(e,t);return this.getDocument(U,r)}parseDocument(e){return this.parseValue({mapValue:{fields:e.fields}})}parseValue(e){if("stringValue"in e)return e.stringValue;if("integerValue"in e)return parseInt(e.integerValue,10);if("doubleValue"in e)return e.doubleValue;if("booleanValue"in e)return e.booleanValue;if("nullValue"in e)return null;if("timestampValue"in e)return this.parseTimestamp(e.timestampValue);if("mapValue"in e){const t={},r=e.mapValue.fields||{};for(const[e,a]of Object.entries(r))t[e]=this.parseValue(a);return t}return"arrayValue"in e?(e.arrayValue.values||[]).map(e=>this.parseValue(e)):null}parseTimestamp(e){const t=new Date(e);return{seconds:Math.floor(t.getTime()/1e3),nanoseconds:t.getTime()%1e3*1e6}}getFromCache(e,t=!1){const r=this.cache.get(e);return r&&(Date.now()<r.expiresAt||t)?r.data:null}setCache(e,t){const r=Date.now();this.cache.set(e,{data:t,timestamp:r,expiresAt:r+this.config.cacheDuration})}clearCache(){this.cache.clear(),this.logger.debug("Firestore cache cleared")}clearCacheEntry(e,t){this.cache.delete(`${e}/${t}`)}getCacheStats(){return{size:this.cache.size,keys:Array.from(this.cache.keys())}}getConfig(){return Object.assign({},this.config)}setChannel(e){this.config.channel=e,this.clearCache()}isConfigured(){return!!(this.config.projectId&&this.config.appId&&this.config.channel)}}_.instance=null;class R{constructor(e){this.firestoreClient=e,this.logger=w.getInstance()}async checkForUpdates(e){const{currentVersion:t,deviceInfo:r,checkDeltas:a=!0}=e;try{e.forceRefresh&&this.firestoreClient.clearCache();const i=await this.firestoreClient.getManifest();if(!i)return this.logger.debug("No manifest found"),{updateAvailable:!1};if(!this.isNewerVersion(t,i.current.version))return{updateAvailable:!1,version:i.current.version};const n=await this.checkRolloutEligibility(i.rollout,r);if(!n.eligible)return{updateAvailable:!0,version:i.current.version,rolloutEligible:!1,rolloutReason:n.reason};const s={updateAvailable:!0,version:i.current.version,bundleUrl:i.current.bundleUrl,minNativeVersion:i.current.minNativeVersion,releaseNotes:i.current.releaseNotes,signature:i.current.signature,checksum:i.current.checksum,mandatory:i.current.mandatory,size:i.current.size,rolloutEligible:!0};if(a&&i.deltas){const e=i.deltas[t];e?(s.delta={available:!0,patchUrl:e.patchUrl,patchSize:e.patchSize,patchChecksum:e.patchChecksum,targetChecksum:e.targetChecksum},this.logger.debug("Delta update available",{from:t,to:i.current.version,patchSize:e.patchSize})):s.delta={available:!1}}return s}catch(e){throw this.logger.error("Error checking for updates",e),e}}async checkRolloutEligibility(e,t){var r,a;if(!e.enabled)return{eligible:!0,reason:"Rollout not enabled, all devices eligible"};const i=Date.now();if(i<C(e.startTime).getTime())return{eligible:!1,reason:"Rollout not started yet"};if(e.endTime&&i>C(e.endTime).getTime())return{eligible:!1,reason:"Rollout ended"};if("scheduled"===(null===(r=e.schedule)||void 0===r?void 0:r.type)&&e.schedule.scheduledTime&&i<C(e.schedule.scheduledTime).getTime())return{eligible:!1,reason:"Scheduled for later"};if(e.targetSegments){const r=this.checkSegments(e.targetSegments,t);if(!r.eligible)return r}let n=e.percentage;"gradual"===(null===(a=e.schedule)||void 0===a?void 0:a.type)&&(n=this.calculateGradualPercentage(e));const s=await this.getDevicePercentile(t.deviceId);return s>n?{eligible:!1,reason:`Device percentile ${s.toFixed(1)}% > rollout ${n}%`}:{eligible:!0,reason:"All checks passed"}}checkSegments(e,t){return e.platforms&&e.platforms.length>0&&!e.platforms.includes(t.platform)?{eligible:!1,reason:`Platform ${t.platform} not targeted`}:e.minAppVersion&&this.compareVersions(t.appVersion,e.minAppVersion)<0?{eligible:!1,reason:`App version below minimum ${e.minAppVersion}`}:e.maxAppVersion&&this.compareVersions(t.appVersion,e.maxAppVersion)>0?{eligible:!1,reason:`App version above maximum ${e.maxAppVersion}`}:e.deviceIds&&e.deviceIds.length>0&&!e.deviceIds.includes(t.deviceId)?{eligible:!1,reason:"Device not in whitelist"}:!(e.regions&&e.regions.length>0)||t.region&&e.regions.includes(t.region)?{eligible:!0,reason:"Segment checks passed"}:{eligible:!1,reason:`Region ${t.region||"unknown"} not targeted`}}calculateGradualPercentage(e){var t,r;if(!(null===(t=e.schedule)||void 0===t?void 0:t.gradualSteps)||!(null===(r=e.schedule)||void 0===r?void 0:r.gradualInterval))return e.percentage;const a=C(e.startTime).getTime(),i=(Date.now()-a)/36e5,n=Math.floor(i/e.schedule.gradualInterval),s=e.schedule.gradualSteps;return n>=s.length?s[s.length-1]:s[n]}async getDevicePercentile(e){const t=await this.hashString(e);return this.hashToPercentile(t)}async hashString(e){const t=(new TextEncoder).encode(e),r=await crypto.subtle.digest("SHA-256",t);return Array.from(new Uint8Array(r)).map(e=>e.toString(16).padStart(2,"0")).join("")}hashToPercentile(e){return parseInt(e.substring(0,8),16)/parseInt("ffffffff",16)*100}compareVersions(e,t){const r=e.split("-")[0],a=t.split("-")[0],i=r.split(".").map(Number),n=a.split(".").map(Number);for(let e=0;e<Math.max(i.length,n.length);e++){const t=i[e]||0,r=n[e]||0;if(t>r)return 1;if(t<r)return-1}const s=e.includes("-"),o=t.includes("-");return s&&!o?-1:!s&&o?1:0}isNewerVersion(e,t){return this.compareVersions(e,t)<0}async getManifest(){return this.firestoreClient.getManifest()}async getManifestFor(e,t){return this.firestoreClient.getManifestFor(e,t)}clearCache(){this.firestoreClient.clearCache()}}class O{constructor(){this.VERSION_CHECK_CACHE_KEY="capacitor_native_update_version_cache",this.CACHE_DURATION=3e5,this.preferences=null,this.memoryCache=new Map,this.firestoreClient=null,this.manifestReader=null,this.logger=w.getInstance(),this.configManager=m.getInstance(),this.securityValidator=v.getInstance()}static compareVersions(e,t){try{const[r,a]=e.split("-"),[i,n]=t.split("-"),s=r.split(".").map(Number),o=i.split(".").map(Number);for(let e=0;e<3;e++){const t=s[e]||0,r=o[e]||0;if(t>r)return 1;if(t<r)return-1}return a&&!n?-1:!a&&n?1:a&&n?a.localeCompare(n):0}catch(r){return e===t?0:e>t?1:-1}}static isValidVersion(e){return/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/.test(e)}static shouldUpdate(e,t,r){return!(r&&O.compareVersions(e,r)<0)&&O.compareVersions(e,t)<0}async initialize(){if(this.preferences=this.configManager.get("preferences"),!this.preferences)throw new y(e.ErrorCode.MISSING_DEPENDENCY,"Preferences not configured. Please configure the plugin first.");if("firestore"===this.configManager.get("backendType")){const t=this.configManager.get("firestore");if(!t)throw new y(e.ErrorCode.INVALID_CONFIG,"Firestore configuration required when using firestore backend");this.firestoreClient=new _(t),this.manifestReader=new R(this.firestoreClient),this.logger.debug("Firestore backend initialized")}}async checkForUpdatesFromFirestore(t,r){if(!this.manifestReader)throw new y(e.ErrorCode.INVALID_CONFIG,"Firestore backend not initialized. Call initialize() first or configure firestore backend.");this.securityValidator.validateVersion(t);const a={currentVersion:t,deviceInfo:r,checkDeltas:this.configManager.get("enableDeltaUpdates")};try{const e=await this.manifestReader.checkForUpdates(a);return e&&this.logger.info("Firestore update check completed",{currentVersion:t,latestVersion:e.version,updateAvailable:e.updateAvailable,eligible:e.rolloutEligible}),e}catch(e){return this.logger.error("Failed to check for updates from Firestore",e),null}}async checkForUpdatesAuto(t,r){if("firestore"===(this.configManager.get("backendType")||"http")){if(!r)throw new y(e.ErrorCode.INVALID_CONFIG,"Device info is required for Firestore backend");return this.checkForUpdatesFromFirestore(t,r)}const a=this.configManager.get("serverUrl"),i=this.configManager.get("channel")||"production",n=this.configManager.get("appId");if(!a||!n)throw new y(e.ErrorCode.INVALID_CONFIG,"Server URL and App ID are required for HTTP backend");return this.checkForUpdates(a,i,t,n)}async checkForUpdates(t,r,a,i){if(this.securityValidator.validateUrl(t),this.securityValidator.validateVersion(a),!r||!i)throw new y(e.ErrorCode.INVALID_CONFIG,"Channel and appId are required");const n=`${r}-${i}`,s=await this.getCachedVersionInfo(n);if(s&&s.channel===r&&Date.now()-s.timestamp<this.CACHE_DURATION)return this.logger.debug("Returning cached version info",{channel:r,version:s.data.version}),s.data;try{const s=new URL(`${t}/check`);s.searchParams.append("channel",r),s.searchParams.append("version",a),s.searchParams.append("appId",i),s.searchParams.append("platform","web");const o=await fetch(s.toString(),{method:"GET",headers:{"Content-Type":"application/json","X-App-Version":a,"X-App-Id":i},signal:AbortSignal.timeout(this.configManager.get("downloadTimeout"))});if(!o.ok)throw new Error(`Version check failed: ${o.status}`);const l=await o.json();if(!l.version)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"No version in server response");return this.securityValidator.validateVersion(l.version),l.bundleUrl&&this.securityValidator.validateUrl(l.bundleUrl),l.minAppVersion&&this.securityValidator.validateVersion(l.minAppVersion),await this.cacheVersionInfo(n,r,l),this.logger.info("Version check completed",{channel:r,currentVersion:a,latestVersion:l.version,updateAvailable:this.isNewerVersion(l.version,a)}),l}catch(e){return this.logger.error("Failed to check for updates",e),null}}isNewerVersion(e,t){try{const r=this.parseVersion(e),a=this.parseVersion(t);return r.major!==a.major?r.major>a.major:r.minor!==a.minor?r.minor>a.minor:r.patch!==a.patch?r.patch>a.patch:!(r.prerelease&&!a.prerelease||(r.prerelease||!a.prerelease)&&(!r.prerelease||!a.prerelease||!(r.prerelease>a.prerelease)))}catch(r){return this.logger.error("Failed to compare versions",{version1:e,version2:t,error:r}),!1}}isUpdateMandatory(e,t){if(!t)return!1;try{return this.securityValidator.validateVersion(e),this.securityValidator.validateVersion(t),!this.isNewerVersion(e,t)&&e!==t}catch(e){return this.logger.error("Failed to check mandatory update",e),!1}}parseVersion(t){const r=t.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/);if(!r)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Invalid version format");return{major:parseInt(r[1],10),minor:parseInt(r[2],10),patch:parseInt(r[3],10),prerelease:r[4],build:r[5]}}buildVersionString(e){let t=`${e.major}.${e.minor}.${e.patch}`;return e.prerelease&&(t+=`-${e.prerelease}`),e.build&&(t+=`+${e.build}`),t}isCompatibleWithNativeVersion(e,t,r){if(!r)return!0;try{const a=r[e];return!a||(this.securityValidator.validateVersion(t),this.securityValidator.validateVersion(a),!this.isNewerVersion(a,t))}catch(e){return this.logger.error("Failed to check compatibility",e),!1}}async getCachedVersionInfo(e){const t=this.memoryCache.get(e);if(t&&Date.now()-t.timestamp<this.CACHE_DURATION)return t;try{const{value:t}=await this.preferences.get({key:this.VERSION_CHECK_CACHE_KEY});if(!t)return null;const r=JSON.parse(t)[e];if(r&&Date.now()-r.timestamp<this.CACHE_DURATION)return this.memoryCache.set(e,r),r}catch(e){this.logger.debug("Failed to load cached version info",e)}return null}async cacheVersionInfo(e,t,r){const a={channel:t,data:r,timestamp:Date.now()};this.memoryCache.set(e,a);try{const{value:t}=await this.preferences.get({key:this.VERSION_CHECK_CACHE_KEY}),r=t?JSON.parse(t):{},i=Date.now();for(const e in r)i-r[e].timestamp>2*this.CACHE_DURATION&&delete r[e];r[e]=a,await this.preferences.set({key:this.VERSION_CHECK_CACHE_KEY,value:JSON.stringify(r)})}catch(e){this.logger.warn("Failed to cache version info",e)}}async clearVersionCache(){this.memoryCache.clear();try{await this.preferences.remove({key:this.VERSION_CHECK_CACHE_KEY})}catch(e){this.logger.warn("Failed to clear version cache",e)}}shouldBlockDowngrade(e,t){try{return this.securityValidator.isVersionDowngrade(e,t)}catch(e){return this.logger.error("Failed to check downgrade",e),!0}}}class S{constructor(e){this.config=e,this.logger=new w("AppUpdateChecker")}async checkServerVersion(e){if(!this.config.updateUrl)return{};try{const e=new URL(`${this.config.updateUrl}/app-version`);e.searchParams.append("platform",this.getPlatform()),e.searchParams.append("current",await this.getCurrentVersion()),this.config.channel&&e.searchParams.append("channel",this.config.channel);const t=await fetch(e.toString(),{method:"GET",headers:{Accept:"application/json","X-App-Version":await this.getCurrentVersion(),"X-App-Platform":this.getPlatform()}});if(!t.ok)throw new Error(`Server returned ${t.status}`);const r=await t.json();return{availableVersion:r.version,updatePriority:r.priority,releaseNotes:r.releaseNotes,updateSize:r.size,updateURL:r.downloadUrl}}catch(e){return this.logger.error("Failed to check server version",e),{}}}compareVersions(e,t){const r=e.split(".").map(Number),a=t.split(".").map(Number);for(let e=0;e<Math.max(r.length,a.length);e++){const t=r[e]||0,i=a[e]||0;if(t>i)return 1;if(t<i)return-1}return 0}isUpdateRequired(e,t,r){return this.compareVersions(e,t)<0||!!(r&&this.compareVersions(e,r)<0)}determineUpdatePriority(e,t){const[r,a]=e.split(".").map(Number);return r>0?5:a>0&&t&&t>30?4:a>0?3:1}async getCurrentVersion(){return"1.0.0"}getPlatform(){if("undefined"!=typeof window){const e=window.navigator.userAgent;if(/android/i.test(e))return"android";if(/iPad|iPhone|iPod/.test(e))return"ios"}return"web"}}var T;!function(e){e[e.UNKNOWN=0]="UNKNOWN",e[e.PENDING=1]="PENDING",e[e.DOWNLOADING=2]="DOWNLOADING",e[e.INSTALLING=3]="INSTALLING",e[e.INSTALLED=4]="INSTALLED",e[e.FAILED=5]="FAILED",e[e.CANCELED=6]="CANCELED",e[e.DOWNLOADED=11]="DOWNLOADED"}(T||(T={}));class M{constructor(e){this.logger=new w("AppUpdateInstaller"),this.currentState={installStatus:T.UNKNOWN,packageName:"",availableVersion:""}}async startImmediateUpdate(){if(this.logger.log("Starting immediate update installation"),this.updateState(T.PENDING),this.isAndroid())this.logger.log("Triggering Android immediate update");else{if(!this.isIOS())throw new Error("Immediate updates not supported on web platform");this.logger.log("Opening iOS App Store for update")}}async startFlexibleUpdate(){if(this.logger.log("Starting flexible update download"),this.updateState(T.DOWNLOADING),this.isWeb())this.simulateFlexibleUpdate();else{if(!this.isAndroid())throw new Error("Flexible updates not supported on iOS");this.logger.log("Starting Android flexible update")}}async completeFlexibleUpdate(){if(this.logger.log("Completing flexible update installation"),this.currentState.installStatus!==T.DOWNLOADED)throw new Error("Update not ready for installation");this.updateState(T.INSTALLING),this.isAndroid()?this.logger.log("Completing Android update installation"):setTimeout(()=>{this.updateState(T.INSTALLED)},1e3)}async cancelUpdate(){this.logger.log("Cancelling update"),this.currentState.installStatus===T.DOWNLOADING&&this.updateState(T.CANCELED)}async getInstallState(){return Object.assign({},this.currentState)}onProgress(e){this.progressCallback=e}updateState(e,t){this.currentState.installStatus=e,void 0!==t&&(this.currentState.installErrorCode=t),this.logger.log("Update state changed",this.currentState)}simulateFlexibleUpdate(){let e=0;const t=52428800,r=1048576,a=setInterval(()=>{e+=r,e>=t&&(e=t,clearInterval(a),this.updateState(T.DOWNLOADED));const i={bytesDownloaded:e,totalBytesToDownload:t,percentComplete:Math.round(e/t*100),downloadSpeed:r,estimatedTime:Math.ceil((t-e)/r)};this.progressCallback&&this.progressCallback(i)},1e3)}isAndroid(){return"undefined"!=typeof window&&/android/i.test(window.navigator.userAgent)}isIOS(){return"undefined"!=typeof window&&/iPad|iPhone|iPod/.test(window.navigator.userAgent)}isWeb(){return!this.isAndroid()&&!this.isIOS()}}class V{constructor(e){this.config=e,this.logger=new w("PlatformAppUpdate"),this.platform=t.Capacitor.getPlatform()}async checkForUpdate(e){this.logger.log("Checking for platform update: "+this.platform);const t=await this.getVersionInfo(),r={updateAvailable:!1,currentVersion:t.currentVersion,availableVersion:t.currentVersion};if("android"===this.platform)return r;if("ios"===this.platform)return r;if(this.config.webUpdateUrl)try{const e=await fetch(this.config.webUpdateUrl),a=await e.json();a.version&&a.version!==t.currentVersion&&(r.updateAvailable=!0,r.availableVersion=a.version,r.releaseNotes=a.releaseNotes,r.updateURL=a.downloadUrl)}catch(e){this.logger.error("Failed to check web update",e)}return r}async getVersionInfo(){return{currentVersion:"1.0.0",buildNumber:"1",packageName:"com.example.app",platform:this.platform,minimumVersion:this.config.minimumVersion}}async getAppStoreUrl(){const e=this.platform;let t="";if("ios"===e){const e=this.config.appStoreId||this.config.iosAppId;if(!e)throw new Error("App Store ID not configured");t=`https://apps.apple.com/app/id${e}`}else t="android"===e?`https://play.google.com/store/apps/details?id=${this.config.packageName||(await this.getVersionInfo()).packageName}`:this.config.webUpdateUrl||window.location.origin;return{url:t,platform:e}}async openUrl(e){if("undefined"==typeof window||!window.open)throw new Error("Cannot open URL on this platform");window.open(e,"_blank")}isUpdateSupported(){return"android"===this.platform||"ios"!==this.platform}getUpdateCapabilities(){const e={immediateUpdate:!1,flexibleUpdate:!1,backgroundDownload:!1,inAppReview:!1};return"android"===this.platform?(e.immediateUpdate=!0,e.flexibleUpdate=!0,e.backgroundDownload=!0,e.inAppReview=!0):"ios"===this.platform&&(e.inAppReview=!0),e}}class B{constructor(e){this.listeners=new Map,this.config=e,this.logger=new w("AppUpdateManager"),this.checker=new S(e),this.installer=new M(e),this.platformUpdate=new V(e)}async checkAppUpdate(e){try{this.logger.log("Checking for app updates",e);const t=await this.platformUpdate.checkForUpdate(e);if(!t.updateAvailable&&this.config.updateUrl){const r=await this.checker.checkServerVersion(e);return this.mergeUpdateInfo(t,r)}return t}catch(e){throw this.logger.error("Failed to check app update",e),e}}async startImmediateUpdate(){try{this.logger.log("Starting immediate update");const e=await this.checkAppUpdate();if(!e.immediateUpdateAllowed)throw new Error("Immediate update not allowed");await this.installer.startImmediateUpdate(),this.emit("appUpdateStateChanged",{installStatus:1,packageName:this.config.packageName||"",availableVersion:e.availableVersion||""})}catch(e){throw this.logger.error("Failed to start immediate update",e),e}}async startFlexibleUpdate(){try{this.logger.log("Starting flexible update");const e=await this.checkAppUpdate();if(!e.flexibleUpdateAllowed)throw new Error("Flexible update not allowed");await this.installer.startFlexibleUpdate(),this.installer.onProgress(e=>{this.emit("appUpdateProgress",e)}),this.emit("appUpdateStateChanged",{installStatus:2,packageName:this.config.packageName||"",availableVersion:e.availableVersion||""})}catch(e){throw this.logger.error("Failed to start flexible update",e),e}}async completeFlexibleUpdate(){try{this.logger.log("Completing flexible update"),await this.installer.completeFlexibleUpdate(),this.emit("appUpdateStateChanged",{installStatus:3,packageName:this.config.packageName||"",availableVersion:""})}catch(e){throw this.logger.error("Failed to complete flexible update",e),e}}async getVersionInfo(){try{return this.logger.log("Getting version info"),await this.platformUpdate.getVersionInfo()}catch(e){throw this.logger.error("Failed to get version info",e),e}}async isMinimumVersionMet(){try{this.logger.log("Checking minimum version");const e=await this.getVersionInfo(),t=this.config.minimumVersion||"0.0.0",r=this.checker.compareVersions(e.currentVersion,t)>=0;return{isMet:r,currentVersion:e.currentVersion,minimumVersion:t,updateRequired:!r&&!0===this.config.enforceMinVersion}}catch(e){throw this.logger.error("Failed to check minimum version",e),e}}async getAppUpdateInfo(){return this.checkAppUpdate()}async performImmediateUpdate(){return this.startImmediateUpdate()}async openAppStore(e){try{this.logger.log("Opening app store");const e=await this.getAppStoreUrl();await this.platformUpdate.openUrl(e.url)}catch(e){throw this.logger.error("Failed to open app store",e),e}}async getAppStoreUrl(){try{return this.logger.log("Getting app store URL"),await this.platformUpdate.getAppStoreUrl()}catch(e){throw this.logger.error("Failed to get app store URL",e),e}}async getUpdateInstallState(){try{return this.logger.log("Getting update install state"),await this.installer.getInstallState()}catch(e){throw this.logger.error("Failed to get update install state",e),e}}addListener(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),{remove:async()=>{const r=this.listeners.get(e);r&&r.delete(t)}}}async removeAllListeners(e){e?this.listeners.delete(e):this.listeners.clear()}emit(e,t){const r=this.listeners.get(e);r&&r.forEach(r=>{try{r(t)}catch(t){this.logger.error(`Error in ${e} listener`,t)}})}mergeUpdateInfo(e,t){return Object.assign(Object.assign(Object.assign({},e),t),{updateAvailable:e.updateAvailable||!!t.availableVersion,availableVersion:t.availableVersion||e.availableVersion})}}class F{constructor(){this.listeners=new Map}static getInstance(){return F.instance||(F.instance=new F),F.instance}addListener(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{const r=this.listeners.get(e);r&&(r.delete(t),0===r.size&&this.listeners.delete(e))}}emit(e,t){const r=this.listeners.get(e);r&&r.forEach(r=>{try{r(t)}catch(t){console.error(`Error in event listener for ${e}:`,t)}})}removeListeners(e){this.listeners.delete(e)}removeAllListeners(){this.listeners.clear()}listenerCount(e){var t;return(null===(t=this.listeners.get(e))||void 0===t?void 0:t.size)||0}eventNames(){return Array.from(this.listeners.keys())}}class k{constructor(){this.bundleManager=null,this.downloadManager=null,this.versionManager=null,this.appUpdateManager=null,this.initialized=!1,this.configManager=m.getInstance(),this.logger=w.getInstance(),this.securityValidator=v.getInstance(),this.eventEmitter=F.getInstance()}static getInstance(){return k.instance||(k.instance=new k),k.instance}async initialize(e){var t,i;if(this.initialized)this.logger.warn("Plugin already initialized");else try{this.configManager.configure(e),e.filesystem||(e.filesystem=r.Filesystem),e.preferences||(e.preferences=a.Preferences),this.bundleManager=new N,await this.bundleManager.initialize(),this.downloadManager=new b,await this.downloadManager.initialize(),this.versionManager=new O,await this.versionManager.initialize(),this.appUpdateManager=new B({serverUrl:e.serverUrl||e.baseUrl||"",channel:e.channel||"production",autoCheck:null===(t=e.autoCheck)||void 0===t||t,autoUpdate:null!==(i=e.autoUpdate)&&void 0!==i&&i,updateStrategy:e.updateStrategy,publicKey:e.publicKey,requireSignature:e.requireSignature,checksumAlgorithm:e.checksumAlgorithm,checkInterval:e.checkInterval,security:e.security}),this.setupAppUpdateEventBridge(),this.initialized=!0,this.logger.info("Plugin initialized successfully")}catch(e){throw this.logger.error("Failed to initialize plugin",e),e}}isInitialized(){return this.initialized&&this.configManager.isConfigured()}ensureInitialized(){if(!this.isInitialized())throw new E(e.ErrorCode.NOT_CONFIGURED,"Plugin not initialized. Please call initialize() first.")}getBundleManager(){return this.ensureInitialized(),this.bundleManager}getDownloadManager(){return this.ensureInitialized(),this.downloadManager}getVersionManager(){return this.ensureInitialized(),this.versionManager}getConfigManager(){return this.configManager}getLogger(){return this.logger}getSecurityValidator(){return this.securityValidator}getAppUpdateManager(){return this.ensureInitialized(),this.appUpdateManager}getEventEmitter(){return this.eventEmitter}async reset(){this.logger.info("Resetting plugin state"),this.bundleManager&&await this.bundleManager.clearAllBundles(),this.versionManager&&await this.versionManager.clearVersionCache(),this.downloadManager&&this.downloadManager.cancelAllDownloads(),this.bundleManager=null,this.downloadManager=null,this.versionManager=null,this.appUpdateManager=null,this.initialized=!1,this.eventEmitter.removeAllListeners(),this.logger.info("Plugin reset complete")}async cleanup(){this.logger.info("Cleaning up plugin resources"),this.downloadManager&&this.downloadManager.cancelAllDownloads(),this.bundleManager&&await this.bundleManager.cleanExpiredBundles(),this.logger.info("Cleanup complete")}setupAppUpdateEventBridge(){this.appUpdateManager&&(this.appUpdateManager.addListener("appUpdateStateChanged",e=>{this.eventEmitter.emit("appUpdateStateChanged",e)}),this.appUpdateManager.addListener("appUpdateProgress",e=>{this.eventEmitter.emit("appUpdateProgress",e)}))}}class P{constructor(){this.initialized=!1,this.windowEventListeners=new Map,this.pluginManager=k.getInstance()}async initialize(e){await this.pluginManager.initialize(e),this.initialized=!0,this.setupWindowEventBridge()}isInitialized(){return this.initialized&&this.pluginManager.isInitialized()}async reset(){await this.pluginManager.reset()}async cleanup(){await this.pluginManager.cleanup()}async configure(e){var t;let r;r="config"in e&&"object"==typeof e.config?e.config:{baseUrl:null===(t=e.liveUpdate)||void 0===t?void 0:t.serverUrl},this.initialized?this.pluginManager.getConfigManager().configure(r):await this.initialize(r)}async getSecurityInfo(){return{enforceHttps:!0,certificatePinning:{enabled:!1,pins:[]},validateInputs:!0,secureStorage:!0}}async sync(e){const t=this.pluginManager.getBundleManager();try{const e=await t.getActiveBundle();return{status:d.UP_TO_DATE,version:(null==e?void 0:e.version)||"1.0.0"}}catch(e){return{status:d.ERROR,error:{code:g.UNKNOWN_ERROR,message:e instanceof Error?e.message:"Sync failed"}}}}async download(e){const t=this.pluginManager.getDownloadManager(),r=this.pluginManager.getBundleManager(),a=await t.downloadWithRetry(e.url,e.version),i=await t.saveBlob(e.version,a),n={bundleId:e.version,version:e.version,path:i,downloadTime:Date.now(),size:a.size,status:h.READY,checksum:e.checksum,signature:e.signature,verified:!1};return await r.saveBundleInfo(n),n}async set(e){const t=this.pluginManager.getBundleManager();await t.setActiveBundle(e.bundleId)}async reload(){"undefined"!=typeof window&&window.location.reload()}async current(){const t=this.pluginManager.getBundleManager(),r=await t.getActiveBundle();if(!r)throw new E(e.ErrorCode.FILE_NOT_FOUND,"No active bundle found");return r}async list(){return this.pluginManager.getBundleManager().getAllBundles()}async delete(e){const t=this.pluginManager.getBundleManager();if(e.bundleId)await t.deleteBundle(e.bundleId);else if(void 0!==e.keepVersions){const r=(await t.getAllBundles()).sort((e,t)=>t.downloadTime-e.downloadTime);for(let a=e.keepVersions;a<r.length;a++)await t.deleteBundle(r[a].bundleId)}}async notifyAppReady(){const e=this.pluginManager.getBundleManager(),t=await e.getActiveBundle();t&&(t.status=h.ACTIVE,await e.saveBundleInfo(t))}async getLatest(){return{available:!1}}async setChannel(e){const t=this.pluginManager.getConfigManager().get("preferences");t&&await t.set({key:"update_channel",value:e})}async setUpdateUrl(e){this.pluginManager.getConfigManager().configure({baseUrl:e})}async validateUpdate(e){const t=this.pluginManager.getSecurityValidator();try{const r=await t.validateChecksum(new ArrayBuffer(0),e.checksum);return{isValid:r,details:{checksumValid:r,signatureValid:!0,sizeValid:!0,versionValid:!0}}}catch(e){return{isValid:!1,error:e instanceof Error?e.message:"Validation failed"}}}async getAppUpdateInfo(){return this.pluginManager.getAppUpdateManager().getAppUpdateInfo()}async performImmediateUpdate(){return this.pluginManager.getAppUpdateManager().performImmediateUpdate()}async startFlexibleUpdate(){return this.pluginManager.getAppUpdateManager().startFlexibleUpdate()}async completeFlexibleUpdate(){return this.pluginManager.getAppUpdateManager().completeFlexibleUpdate()}async openAppStore(e){return this.pluginManager.getAppUpdateManager().openAppStore(e)}async requestReview(){return{displayed:!1,error:"Reviews are not supported on web"}}async canRequestReview(){return{canRequest:!1,reason:"Reviews are not supported on web"}}async enableBackgroundUpdates(e){const t=this.pluginManager.getConfigManager().get("preferences");t&&await t.set({key:"background_update_config",value:JSON.stringify(e)})}async disableBackgroundUpdates(){const e=this.pluginManager.getConfigManager().get("preferences");e&&await e.remove({key:"background_update_config"})}async getBackgroundUpdateStatus(){return{enabled:!1,isRunning:!1,checkCount:0,failureCount:0}}async scheduleBackgroundCheck(t){throw new E(e.ErrorCode.PLATFORM_NOT_SUPPORTED,"Background updates are not supported on web")}async triggerBackgroundCheck(){return{success:!1,updatesFound:!1,notificationSent:!1,error:{code:g.PLATFORM_NOT_SUPPORTED,message:"Background updates are not supported on web"}}}async setNotificationPreferences(e){const t=this.pluginManager.getConfigManager().get("preferences");t&&await t.set({key:"notification_preferences",value:JSON.stringify(e)})}async getNotificationPermissions(){return{granted:!1,canRequest:!1}}async requestNotificationPermissions(){return!1}async addListener(e,t){const r=this.pluginManager.getEventEmitter().addListener(e,t);return{remove:async()=>{r()}}}async removeAllListeners(){this.pluginManager.getEventEmitter().removeAllListeners()}setupWindowEventBridge(){const e=this.pluginManager.getEventEmitter();["appUpdateAvailable","appUpdateProgress","appUpdateReady","appUpdateFailed","appUpdateNotificationClicked","appUpdateInstallClicked"].forEach(t=>{const r=r=>{e.emit(t,r.detail)};window.addEventListener(t,r),this.windowEventListeners.set(t,r)})}}const z=t.registerPlugin("NativeUpdate",{web:()=>new P});e.BundleManager=N,e.CacheManager=class{constructor(){this.filesystem=null,this.memoryCache=new Map,this.CACHE_DIR="cache",this.logger=w.getInstance(),this.configManager=m.getInstance()}async initialize(){if(this.filesystem=this.configManager.get("filesystem"),!this.filesystem)throw new Error("Filesystem not configured");try{await this.filesystem.mkdir({path:this.CACHE_DIR,directory:r.Directory.Data,recursive:!0})}catch(e){this.logger.debug("Cache directory may already exist",e)}await this.cleanExpiredCache()}async set(e,t,r){const a=Date.now()+(r||this.configManager.get("cacheExpiration")),i={data:t,timestamp:Date.now(),expiry:a};this.memoryCache.set(e,i),this.shouldPersist(t)&&await this.persistToFile(e,i),this.logger.debug("Cache entry set",{key:e,expiry:new Date(a)})}async get(e){const t=this.memoryCache.get(e);if(t){if(Date.now()<t.expiry)return t.data;this.memoryCache.delete(e)}const r=await this.loadFromFile(e);if(r){if(Date.now()<r.expiry)return this.memoryCache.set(e,r),r.data;await this.removeFile(e)}return null}async has(e){return null!==await this.get(e)}async remove(e){this.memoryCache.delete(e),await this.removeFile(e),this.logger.debug("Cache entry removed",{key:e})}async clear(){this.memoryCache.clear();try{await this.filesystem.rmdir({path:this.CACHE_DIR,directory:r.Directory.Data,recursive:!0}),await this.filesystem.mkdir({path:this.CACHE_DIR,directory:r.Directory.Data,recursive:!0})}catch(e){this.logger.warn("Failed to clear cache directory",e)}this.logger.info("Cache cleared")}async cleanExpiredCache(){const e=Date.now();let t=0;for(const[r,a]of this.memoryCache)e>=a.expiry&&(this.memoryCache.delete(r),t++);try{const a=await this.filesystem.readdir({path:this.CACHE_DIR,directory:r.Directory.Data});for(const r of a.files){const a=r.name.replace(".json",""),i=await this.loadFromFile(a);(!i||e>=i.expiry)&&(await this.removeFile(a),t++)}}catch(e){this.logger.debug("Failed to clean filesystem cache",e)}t>0&&this.logger.info("Cleaned expired cache entries",{count:t})}async getStats(){let e=0,t=0;try{const a=await this.filesystem.readdir({path:this.CACHE_DIR,directory:r.Directory.Data});e=a.files.length;for(const e of a.files)t+=(await this.filesystem.stat({path:`${this.CACHE_DIR}/${e.name}`,directory:r.Directory.Data})).size||0}catch(e){this.logger.debug("Failed to get cache stats",e)}return{memoryEntries:this.memoryCache.size,fileEntries:e,totalSize:t}}async cacheBundleMetadata(e){const t=`bundle_meta_${e.bundleId}`;await this.set(t,e,864e5)}async getCachedBundleMetadata(e){return this.get(`bundle_meta_${e}`)}shouldPersist(e){return"object"==typeof e||"string"==typeof e&&e.length>1024}async persistToFile(e,t){if(this.filesystem)try{const a=`${this.CACHE_DIR}/${e}.json`,i=JSON.stringify(t);await this.filesystem.writeFile({path:a,data:i,directory:r.Directory.Data,encoding:r.Encoding.UTF8})}catch(t){this.logger.warn("Failed to persist cache to file",{key:e,error:t})}}async loadFromFile(e){if(!this.filesystem)return null;try{const t=`${this.CACHE_DIR}/${e}.json`,a=await this.filesystem.readFile({path:t,directory:r.Directory.Data,encoding:r.Encoding.UTF8});return JSON.parse(a.data)}catch(e){return null}}async removeFile(e){if(this.filesystem)try{const t=`${this.CACHE_DIR}/${e}.json`;await this.filesystem.deleteFile({path:t,directory:r.Directory.Data})}catch(t){this.logger.debug("Failed to remove cache file",{key:e,error:t})}}},e.ConfigManager=m,e.ConfigurationError=class extends E{constructor(t,r){super(e.ErrorCode.INVALID_CONFIG,t,r),this.name="ConfigurationError"}},e.DownloadError=I,e.DownloadManager=b,e.Logger=w,e.NativeUpdate=z,e.NativeUpdateError=E,e.PluginManager=k,e.SecurityValidator=v,e.StorageError=A,e.UpdateErrorClass=D,e.UpdateManager=class{constructor(){this.filesystem=null,this.updateInProgress=!1,this.currentState=null,this.pluginManager=k.getInstance(),this.securityValidator=v.getInstance()}async initialize(){if(this.filesystem=this.pluginManager.getConfigManager().get("filesystem"),!this.filesystem)throw new D(e.ErrorCode.MISSING_DEPENDENCY,"Filesystem not configured")}async applyUpdate(t,r){if(this.updateInProgress)throw new D(e.ErrorCode.UPDATE_FAILED,"Another update is already in progress");const a=this.pluginManager.getLogger(),i=this.pluginManager.getBundleManager();try{this.updateInProgress=!0,a.info("Starting bundle update",{bundleId:t});const n=await i.getBundle(t);if(!n)throw new D(e.ErrorCode.FILE_NOT_FOUND,`Bundle ${t} not found`);if("READY"!==n.status&&"ACTIVE"!==n.status)throw new D(e.ErrorCode.BUNDLE_NOT_READY,`Bundle ${t} is not ready for installation`);const s=await i.getActiveBundle();this.currentState={currentBundle:s,newBundle:n,backupPath:null,startTime:Date.now()},await this.validateUpdate(s,n,r),s&&"default"!==s.bundleId&&(this.currentState.backupPath=await this.createBackup(s)),await this.performUpdate(n),await this.verifyUpdate(n),await i.setActiveBundle(t),(null==r?void 0:r.cleanupOldBundles)&&await i.cleanupOldBundles(r.keepBundleCount||3),a.info("Bundle update completed successfully",{bundleId:t,version:n.version,duration:Date.now()-this.currentState.startTime}),this.currentState=null}catch(e){throw a.error("Bundle update failed",e),this.currentState&&await this.rollback(),e}finally{this.updateInProgress=!1}}async validateUpdate(t,r,a){const i=this.pluginManager.getLogger(),n=this.pluginManager.getVersionManager();if(t&&!(null==a?void 0:a.allowDowngrade)&&n.shouldBlockDowngrade(t.version,r.version))throw new y(e.ErrorCode.VERSION_DOWNGRADE,`Cannot downgrade from ${t.version} to ${r.version}`);if(!r.verified){i.warn("Bundle not verified, verifying now",{bundleId:r.bundleId});const t=this.pluginManager.getDownloadManager(),a=await t.loadBlob(r.bundleId);if(!a)throw new D(e.ErrorCode.FILE_NOT_FOUND,"Bundle data not found");const n=await a.arrayBuffer();if(!await this.securityValidator.verifyChecksum(n,r.checksum))throw new y(e.ErrorCode.CHECKSUM_MISMATCH,"Bundle checksum verification failed");if(r.signature&&!await this.securityValidator.verifySignature(n,r.signature))throw new y(e.ErrorCode.SIGNATURE_INVALID,"Bundle signature verification failed");await this.pluginManager.getBundleManager().markBundleAsVerified(r.bundleId)}i.debug("Bundle validation passed",{bundleId:r.bundleId})}async createBackup(t){const a=`backups/${t.bundleId}_${Date.now()}`,i=this.pluginManager.getLogger();try{return await this.filesystem.mkdir({path:a,directory:r.Directory.Data,recursive:!0}),await this.filesystem.copy({from:t.path,to:a,directory:r.Directory.Data}),i.info("Backup created",{bundleId:t.bundleId,backupPath:a}),a}catch(t){throw i.error("Failed to create backup",t),new D(e.ErrorCode.UPDATE_FAILED,"Failed to create backup",void 0,t)}}async performUpdate(t){const a=this.pluginManager.getLogger();try{const e=`active/${t.bundleId}`;await this.filesystem.mkdir({path:e,directory:r.Directory.Data,recursive:!0}),await this.filesystem.copy({from:t.path,to:e,directory:r.Directory.Data}),t.path=e,a.debug("Bundle files installed",{bundleId:t.bundleId,targetPath:e})}catch(t){throw new D(e.ErrorCode.UPDATE_FAILED,"Failed to install bundle files",void 0,t)}}async verifyUpdate(t){try{const e=`${t.path}/index.html`;await this.filesystem.stat({path:e,directory:r.Directory.Data})}catch(t){throw new D(e.ErrorCode.UPDATE_FAILED,"Bundle verification failed after installation",void 0,t)}}async rollback(){var t;if(!this.currentState)throw new D(e.ErrorCode.ROLLBACK_FAILED,"No update state to rollback");const a=this.pluginManager.getLogger();a.warn("Starting rollback",{from:this.currentState.newBundle.bundleId,to:(null===(t=this.currentState.currentBundle)||void 0===t?void 0:t.bundleId)||"default"});try{const e=this.pluginManager.getBundleManager();if(this.currentState.backupPath&&this.currentState.currentBundle){const t=`active/${this.currentState.currentBundle.bundleId}`;await this.filesystem.copy({from:this.currentState.backupPath,to:t,directory:r.Directory.Data}),this.currentState.currentBundle.path=t,await e.saveBundleInfo(this.currentState.currentBundle)}this.currentState.currentBundle?await e.setActiveBundle(this.currentState.currentBundle.bundleId):await e.clearActiveBundle(),a.info("Rollback completed successfully")}catch(t){throw a.error("Rollback failed",t),new D(e.ErrorCode.ROLLBACK_FAILED,"Failed to rollback update",void 0,t)}finally{if(this.currentState.backupPath)try{await this.filesystem.rmdir({path:this.currentState.backupPath,directory:r.Directory.Data,recursive:!0})}catch(e){a.warn("Failed to clean up backup",e)}}}getUpdateProgress(){var e,t;return{inProgress:this.updateInProgress,bundleId:null===(e=this.currentState)||void 0===e?void 0:e.newBundle.bundleId,startTime:null===(t=this.currentState)||void 0===t?void 0:t.startTime}}async cancelUpdate(){this.updateInProgress&&this.currentState&&(this.pluginManager.getLogger().warn("Cancelling update",{bundleId:this.currentState.newBundle.bundleId}),await this.rollback(),this.updateInProgress=!1,this.currentState=null)}},e.ValidationError=y,e.VersionManager=O}({},capacitorExports,capacitorFilesystem,capacitorPreferences);
3
3
  //# sourceMappingURL=plugin.js.map
@@ -1,243 +1,442 @@
1
1
  # Project Completion Tracker
2
2
 
3
- **Last Updated**: 2025-12-26
4
- **Project**: Native Update - Capacitor Plugin
5
- **Version**: 1.1.6
6
-
7
- ---
8
-
9
- ## ✅ COMPLETED FEATURES
10
-
11
- ### Core TypeScript Implementation
12
- - [x] Plugin architecture with proper interfaces (`src/definitions.ts`)
13
- - [x] Live update manager (`src/live-update/update-manager.ts`)
14
- - [x] Bundle manager with download and installation (`src/live-update/bundle-manager.ts`)
15
- - [x] Version manager with semantic versioning (`src/live-update/version-manager.ts`)
16
- - [x] Download manager with progress tracking (`src/live-update/download-manager.ts`)
17
- - [x] Certificate pinning for secure connections (`src/live-update/certificate-pinning.ts`)
18
- - [x] App update checker (`src/app-update/app-update-checker.ts`)
19
- - [x] App update installer (`src/app-update/app-update-installer.ts`)
20
- - [x] App update manager (`src/app-update/app-update-manager.ts`)
21
- - [x] App update notifier with UI (`src/app-update/app-update-notifier.ts`)
22
- - [x] Platform app update integration (`src/app-update/platform-app-update.ts`)
23
- - [x] App review manager (`src/app-review/app-review-manager.ts`)
24
- - [x] Platform review handler (`src/app-review/platform-review-handler.ts`)
25
- - [x] Review conditions checker (`src/app-review/review-conditions-checker.ts`)
26
- - [x] Review rate limiter (`src/app-review/review-rate-limiter.ts`)
27
- - [x] Background scheduler (`src/background-update/background-scheduler.ts`)
28
- - [x] Notification manager (`src/background-update/notification-manager.ts`)
29
-
30
- ### Core Infrastructure
31
- - [x] Analytics framework (`src/core/analytics.ts`)
32
- - [x] Cache manager (`src/core/cache-manager.ts`)
33
- - [x] Configuration system (`src/core/config.ts`)
34
- - [x] Error handling (`src/core/errors.ts`)
35
- - [x] Event emitter (`src/core/event-emitter.ts`)
36
- - [x] Logger (`src/core/logger.ts`)
37
- - [x] Performance monitoring (`src/core/performance.ts`)
38
- - [x] Plugin manager (`src/core/plugin-manager.ts`)
39
- - [x] Security utilities (`src/core/security.ts`)
40
-
41
- ### Security Implementation
42
- - [x] Crypto utilities (`src/security/crypto.ts`)
43
- - [x] Input/output validator (`src/security/validator.ts`)
44
- - [x] SHA-256 checksum verification
45
- - [x] RSA/ECDSA signature verification
46
- - [x] HTTPS enforcement
47
- - [x] Certificate pinning architecture
48
-
49
- ### Native Implementations
50
-
51
- #### iOS (Swift)
52
- - [x] Main plugin class (`ios/Plugin/NativeUpdatePlugin.swift`)
53
- - [x] Live update implementation (`ios/Plugin/LiveUpdate/LiveUpdatePlugin.swift`)
54
- - [x] Bundle manager (`ios/Plugin/LiveUpdate/BundleManager.swift`)
55
- - [x] Security manager (`ios/Plugin/Security/SecurityManager.swift`)
56
- - [x] App update plugin (`ios/Plugin/AppUpdate/AppUpdatePlugin.swift`)
57
- - [x] App review plugin (`ios/Plugin/AppReview/AppReviewPlugin.swift`)
58
- - [x] Background update plugin (`ios/Plugin/BackgroundUpdate/BackgroundUpdatePlugin.swift`)
59
-
60
- #### Android (Kotlin)
61
- - [x] Main plugin class (`android/src/main/java/com/aoneahsan/nativeupdate/NativeUpdatePlugin.kt`)
62
- - [x] Live update implementation (`android/src/main/java/com/aoneahsan/nativeupdate/LiveUpdatePlugin.kt`)
63
- - [x] Bundle manager (`android/src/main/java/com/aoneahsan/nativeupdate/BundleManager.kt`)
64
- - [x] Security manager (`android/src/main/java/com/aoneahsan/nativeupdate/SecurityManager.kt`)
65
- - [x] App update plugin (`android/src/main/java/com/aoneahsan/nativeupdate/AppUpdatePlugin.kt`)
66
- - [x] App review plugin (`android/src/main/java/com/aoneahsan/nativeupdate/AppReviewPlugin.kt`)
67
- - [x] Background update plugin (`android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdatePlugin.kt`)
68
-
69
- ### Testing Infrastructure
70
- - [x] Vitest configuration (`vitest.config.ts`)
71
- - [x] Bundle manager tests (`src/__tests__/bundle-manager.test.ts`)
72
- - [x] Config tests (`src/__tests__/config.test.ts`)
73
- - [x] Integration tests (`src/__tests__/integration.test.ts`)
74
- - [x] Security tests (`src/__tests__/security.test.ts`)
75
- - [x] Version manager tests (`src/__tests__/version-manager.test.ts`)
76
-
77
- ### CLI Tools
78
- - [x] Main CLI (`cli/index.js`)
79
- - [x] Init command (`cli/commands/init.js`)
80
- - [x] Bundle create command (`cli/commands/bundle-create.js`)
81
- - [x] Bundle sign command (`cli/commands/bundle-sign.js`)
82
- - [x] Bundle verify command (`cli/commands/bundle-verify.js`)
83
- - [x] Keys generate command (`cli/commands/keys-generate.js`)
84
- - [x] Backend create command (`cli/commands/backend-create.js`)
85
- - [x] Server start command (`cli/commands/server-start.js`)
86
- - [x] Monitor command (`cli/commands/monitor.js`)
87
-
88
- ### Backend Infrastructure
89
-
90
- #### Production Backend (Node.js + SQLite)
91
- - [x] Main server (`production-backend/src/index.js`)
92
- - [x] Database initialization (`production-backend/src/database/init.js`)
93
- - [x] Auth middleware (`production-backend/src/middleware/auth.js`)
94
- - [x] Error middleware (`production-backend/src/middleware/error.js`)
95
- - [x] Logging middleware (`production-backend/src/middleware/logging.js`)
96
- - [x] Validation middleware (`production-backend/src/middleware/validation.js`)
97
- - [x] Analytics routes (`production-backend/src/routes/analytics.js`)
98
- - [x] Auth routes (`production-backend/src/routes/auth.js`)
99
- - [x] Bundles routes (`production-backend/src/routes/bundles.js`)
100
- - [x] Health routes (`production-backend/src/routes/health.js`)
101
- - [x] Updates routes (`production-backend/src/routes/updates.js`)
102
- - [x] Logger utility (`production-backend/src/utils/logger.js`)
103
-
104
- #### Firebase Backend Example
105
- - [x] Firebase Functions (`example-app/firebase-backend/src/index.ts`)
106
- - [x] Auth middleware (`example-app/firebase-backend/src/middleware/auth.ts`)
107
- - [x] Analytics routes (`example-app/firebase-backend/src/routes/analytics.ts`)
108
- - [x] Bundles routes (`example-app/firebase-backend/src/routes/bundles.ts`)
109
- - [x] Updates routes (`example-app/firebase-backend/src/routes/updates.ts`)
110
- - [x] Validation utils (`example-app/firebase-backend/src/utils/validation.ts`)
111
- - [x] Version utils (`example-app/firebase-backend/src/utils/version.ts`)
112
- - [x] Firestore indexes (`example-app/firebase-backend/firestore.indexes.json`)
113
- - [x] Firestore rules (`example-app/firebase-backend/firestore.rules`)
114
- - [x] Storage rules (`example-app/firebase-backend/storage.rules`)
115
-
116
- #### Backend Template (Express)
117
- - [x] Simple server (`backend-template/server.js`)
118
-
119
- ### Documentation
120
- - [x] Main README (`Readme.md`)
121
- - [x] API documentation (`API.md`)
122
- - [x] Changelog (`CHANGELOG.md`)
123
- - [x] Contributing guide (`CONTRIBUTING.md`)
124
- - [x] Security policy (`SECURITY.md`)
125
- - [x] Features overview (`FEATURES.md`)
126
- - [x] Quick start guide (`docs/QUICK_START.md`)
127
- - [x] Live updates guide (`docs/LIVE_UPDATES_GUIDE.md`)
128
- - [x] Native updates guide (`docs/NATIVE_UPDATES_GUIDE.md`)
129
- - [x] App review guide (`docs/APP_REVIEW_GUIDE.md`)
130
- - [x] Bundle signing guide (`docs/BUNDLE_SIGNING.md`)
131
- - [x] Background updates (`docs/background-updates.md`)
132
- - [x] CLI reference (`docs/cli-reference.md`)
133
- - [x] Migration guide (`docs/MIGRATION.md`)
134
- - [x] Production readiness (`docs/production-readiness.md`)
135
- - [x] Server requirements (`docs/server-requirements.md`)
136
- - [x] Installation (`docs/getting-started/installation.md`)
137
- - [x] Configuration (`docs/getting-started/configuration.md`)
138
- - [x] Quick start (`docs/getting-started/quick-start.md`)
139
- - [x] Live updates feature (`docs/features/live-updates.md`)
140
- - [x] App updates feature (`docs/features/app-updates.md`)
141
- - [x] App reviews feature (`docs/features/app-reviews.md`)
142
- - [x] Basic usage examples (`docs/examples/basic-usage.md`)
143
- - [x] Advanced scenarios (`docs/examples/advanced-scenarios.md`)
144
- - [x] Deployment guide (`docs/guides/deployment-guide.md`)
145
- - [x] Key management (`docs/guides/key-management.md`)
146
- - [x] Migration from CodePush (`docs/guides/migration-from-codepush.md`)
147
- - [x] Security best practices (`docs/guides/security-best-practices.md`)
148
- - [x] Testing guide (`docs/guides/testing-guide.md`)
149
- - [x] Certificate pinning (`docs/security/certificate-pinning.md`)
150
- - [x] API references for all modules (`docs/api/`)
151
-
152
- ### Example Applications
153
- - [x] Basic example app (`example/`)
154
- - [x] Advanced example app with Firebase (`example-app/`)
155
- - [x] Test app for development (`test-app/`)
156
-
157
- ### Build & Development Tools
158
- - [x] TypeScript configuration (`tsconfig.json`, `tsconfig.node.json`)
159
- - [x] Rollup bundler config (`rollup.config.js`)
160
- - [x] ESLint config (`eslint.config.js`)
161
- - [x] Prettier config (`.prettierrc`)
162
- - [x] Vitest config (`vitest.config.ts`)
163
- - [x] Package.json with all scripts
164
- - [x] NVM version file (`.nvmrc`)
165
- - [x] EditorConfig (`.editorconfig`)
166
- - [x] Capacitor config (`capacitor.config.ts`)
167
- - [x] CocoaPods spec (`NativeUpdate.podspec`)
168
-
169
- ### Utilities
170
- - [x] Bundle creator tool (`tools/bundle-creator.js`)
171
- - [x] Bundle signer tool (`tools/bundle-signer.js`)
172
- - [x] Server example (`server-example/`)
173
-
174
- ---
175
-
176
- ## ⚠️ PENDING FIXES (MUST COMPLETE NOW)
177
-
178
- ### Code Quality Issues
179
- - [ ] **CRITICAL**: Fix all 40 ESLint warnings (TypeScript `any` types) - MUST FIX NOW
180
- - [ ] **CRITICAL**: Remove placeholder code in `src/core/performance.ts` (storage check) - MUST IMPLEMENT NOW
181
- - [ ] **CRITICAL**: Remove placeholder code in `src/core/security.ts` (certificate pinning note) - MUST CLARIFY NOW
182
- - [ ] **CRITICAL**: Remove placeholder code in `ios/Plugin/LiveUpdate/LiveUpdatePlugin.swift` (file copy & unzip) - MUST IMPLEMENT NOW
183
-
184
- ### Documentation Updates
185
- - [ ] Update `FINAL_STATUS.md` to reflect current TRUE status
186
- - [ ] Update `PRODUCTION_STATUS.md` to reflect current TRUE status
187
- - [ ] Update `REMAINING_FEATURES.md` to reflect ACTUAL remaining work
188
- - [ ] Update `ROADMAP.md` to reflect completed items
189
- - [ ] Create Firebase indexes/rules verification document
190
-
191
- ---
192
-
193
- ## 🚫 NOT APPLICABLE / NOT NEEDED
194
-
195
- ### Items That Don't Apply
196
- - ❌ Vite logging level change (not a Vite project, uses Rollup)
197
- - ❌ Firebase permissions errors in core plugin (Firebase only used in example-app)
198
-
199
- ---
200
-
201
- ## 📊 COMPLETION STATISTICS
202
-
203
- ### Overall Progress
204
- - **Core Plugin**: 100% Complete
205
- - **Native Implementations**: 95% Complete (some placeholders need implementation)
206
- - **CLI Tools**: 100% Complete
207
- - **Backend Examples**: 100% Complete
208
- - **Documentation**: 100% Complete
209
- - **Testing**: 100% Complete
210
- - **Code Quality**: 90% (40 ESLint warnings to fix)
211
-
212
- ### Issues to Resolve
213
- 1. **40 ESLint warnings** - Replace `any` with proper types
214
- 2. **3 code placeholders** - Implement or document as intentional
215
- 3. **Documentation inconsistency** - Status files show conflicting states
216
-
217
- ---
218
-
219
- ## 🎯 IMMEDIATE ACTION ITEMS
220
-
221
- 1. ✅ Fix all 40 ESLint `any` type warnings
222
- 2. ✅ Remove or implement all placeholder code
223
- 3. ✅ Create Firebase tracking document
224
- 4. ✅ Update all status documents for consistency
225
- 5. ✅ Run final build with zero warnings
226
- 6. ✅ Verify no errors or warnings in entire project
227
-
228
- ---
229
-
230
- ## 📝 NOTES
231
-
232
- - This is a **Capacitor plugin package**, not a web app, so:
233
- - No Vite (uses Rollup instead)
234
- - No browser-based development server
235
- - Firebase only used in example-app, not core plugin
3
+ **Last Updated:** 2026-01-16
4
+ **Project:** native-update (Capacitor OTA Update Plugin)
5
+ **Version:** 1.3.3
6
+ **Status:** PRODUCTION READY
236
7
 
237
- - The plugin provides:
238
- - Live/OTA updates for web assets
239
- - Native app store update checking
240
- - In-app review prompts
8
+ This document provides a comprehensive tracking of all project components, their completion status, and verification details.
241
9
 
242
- - Backend implementation is left to users (examples provided)
243
- - Real device testing recommended before production use
10
+ ---
11
+
12
+ ## Executive Summary
13
+
14
+ | Category | Status | Progress |
15
+ |----------|--------|----------|
16
+ | Plugin Core (TypeScript) | ✅ Complete | 100% |
17
+ | iOS Native Implementation | ✅ Complete | 100% |
18
+ | Android Native Implementation | ✅ Complete | 100% |
19
+ | CLI Tools | ✅ Complete | 100% |
20
+ | Marketing Website | ✅ Complete | 100% |
21
+ | Example Applications | ✅ Complete | 100% |
22
+ | Documentation | ✅ Complete | 100% |
23
+ | Firebase Integration | ✅ Complete | 100% |
24
+ | Unit Tests | ✅ Complete | 100% |
25
+ | iOS Native Tests (XCTest) | ✅ Complete | 100% |
26
+ | Android Native Tests (JUnit) | ✅ Complete | 100% |
27
+ | E2E Tests (Detox) | ✅ Complete | 100% |
28
+
29
+ **Overall:** Production ready with comprehensive test coverage.
30
+
31
+ ---
32
+
33
+ ## 1. Plugin Core Implementation
34
+
35
+ ### 1.1 TypeScript Source Files (`/src/`)
36
+
37
+ | Module | Files | Status | Last Verified |
38
+ |--------|-------|--------|---------------|
39
+ | Core Definitions | `definitions.ts`, `index.ts`, `plugin.ts`, `web.ts` | ✅ Complete | 2026-01-16 |
40
+ | Live Update | 7 files in `/src/live-update/` | ✅ Complete | 2026-01-16 |
41
+ | App Update | 6 files in `/src/app-update/` | ✅ Complete | 2026-01-16 |
42
+ | App Review | 5 files in `/src/app-review/` | ✅ Complete | 2026-01-16 |
43
+ | Background Update | 3 files in `/src/background-update/` | ✅ Complete | 2026-01-16 |
44
+ | Core Infrastructure | 9 files in `/src/core/` | ✅ Complete | 2026-01-16 |
45
+ | Security | 2 files in `/src/security/` | ✅ Complete | 2026-01-16 |
46
+ | Firestore Integration | 4 files in `/src/firestore/` | ✅ Complete | 2026-01-16 |
47
+
48
+ ### 1.2 Build Verification
49
+
50
+ | Check | Result | Date |
51
+ |-------|--------|------|
52
+ | `yarn lint` | ✅ 0 warnings | 2026-01-16 |
53
+ | `yarn build` | ✅ 0 errors | 2026-01-16 |
54
+ | TypeScript compilation | ✅ Pass | 2026-01-16 |
55
+ | Rollup bundling | ✅ ESM, CJS, UMD generated | 2026-01-16 |
56
+
57
+ ---
58
+
59
+ ## 2. Native Implementations
60
+
61
+ ### 2.1 iOS (`/ios/Plugin/`)
62
+
63
+ | File | Purpose | Status |
64
+ |------|---------|--------|
65
+ | `NativeUpdatePlugin.swift` | Main plugin bridge | ✅ Complete |
66
+ | `NativeUpdatePlugin.m` | Objective-C bridge | ✅ Complete |
67
+ | `LiveUpdate/LiveUpdatePlugin.swift` | OTA updates | ✅ Complete |
68
+ | `LiveUpdate/WebViewConfiguration.swift` | WebView setup | ✅ Complete |
69
+ | `AppUpdate/AppUpdatePlugin.swift` | App Store updates | ✅ Complete |
70
+ | `AppReview/AppReviewPlugin.swift` | StoreKit reviews | ✅ Complete |
71
+ | `BackgroundUpdate/BackgroundUpdatePlugin.swift` | Background checking | ✅ Complete |
72
+ | `BackgroundUpdate/BackgroundNotificationManager.swift` | Notifications | ✅ Complete |
73
+ | `Security/SecurityManager.swift` | Crypto operations | ✅ Complete |
74
+
75
+ ### 2.2 Android (`/android/`)
76
+
77
+ | Component | Status |
78
+ |-----------|--------|
79
+ | Gradle build configuration | ✅ Complete |
80
+ | NativeUpdatePlugin.kt | ✅ Complete |
81
+ | LiveUpdatePlugin.kt | ✅ Complete |
82
+ | AppUpdatePlugin.kt (Play Core) | ✅ Complete |
83
+ | AppReviewPlugin.kt | ✅ Complete |
84
+ | BackgroundUpdatePlugin.kt | ✅ Complete |
85
+ | SecurityManager.kt | ✅ Complete |
86
+
87
+ ---
88
+
89
+ ## 3. CLI Tools (`/cli/`)
90
+
91
+ | Command | File | Status | Verified |
92
+ |---------|------|--------|----------|
93
+ | `init` | `commands/init.js` | ✅ Complete | 2026-01-16 |
94
+ | `bundle create` | `commands/bundle-create.js` | ✅ Complete | 2026-01-16 |
95
+ | `bundle sign` | `commands/bundle-sign.js` | ✅ Complete | 2026-01-16 |
96
+ | `bundle verify` | `commands/bundle-verify.js` | ✅ Complete | 2026-01-16 |
97
+ | `keys generate` | `commands/keys-generate.js` | ✅ Complete | 2026-01-16 |
98
+ | `backend create` | `commands/backend-create.js` | ✅ Complete | 2026-01-16 |
99
+ | `server start` | `commands/server-start.js` | ✅ Complete | 2026-01-16 |
100
+ | `monitor` | `commands/monitor.js` | ✅ Complete | 2026-01-16 |
101
+
102
+ **CLI Help Verification:** `node cli/index.js --help` ✅ All 8 commands listed
103
+
104
+ **Note:** TODOs in `backend-create.js` are intentional template placeholders for user customization. See `/docs/guides/BACKEND_TEMPLATES_GUIDE.md`.
105
+
106
+ ---
107
+
108
+ ## 4. Marketing Website (`/website/`)
109
+
110
+ ### 4.1 Pages
111
+
112
+ | Category | Pages | Status |
113
+ |----------|-------|--------|
114
+ | Marketing | Home, Features, Pricing, Examples, Docs | ✅ Complete |
115
+ | Legal | Privacy, Terms, Security, Cookies, Data Deletion | ✅ Complete |
116
+ | Info | About, Contact | ✅ Complete |
117
+ | Dashboard | Overview, Apps, Builds, Rollouts, Upload, Analytics, Settings, Config, GoogleDrive, AppDetail | ✅ Complete |
118
+
119
+ ### 4.2 Build Verification
120
+
121
+ | Check | Result | Date |
122
+ |-------|--------|------|
123
+ | `yarn lint` | ✅ 0 warnings | 2026-01-16 |
124
+ | `yarn build` | ✅ 0 errors | 2026-01-16 |
125
+ | Vite logLevel | ✅ Set to 'info' | 2026-01-16 |
126
+
127
+ ### 4.3 Firebase Configuration
128
+
129
+ | File | Status | Last Updated |
130
+ |------|--------|--------------|
131
+ | `firestore.rules` | ✅ Complete | 2026-01-16 |
132
+ | `firestore.indexes.json` | ✅ Complete | 2026-01-16 |
133
+
134
+ ---
135
+
136
+ ## 5. Firebase Rules & Indexes
137
+
138
+ ### 5.1 Website Firestore Indexes (`/website/firestore.indexes.json`)
139
+
140
+ | Collection | Query Pattern | Index Status |
141
+ |------------|---------------|--------------|
142
+ | `apps` | userId + createdAt DESC | ✅ Defined |
143
+ | `builds` | userId + uploadedAt DESC | ✅ Defined |
144
+ | `builds` | userId + appId + uploadedAt DESC | ✅ Defined |
145
+ | `builds` | userId + channel + uploadedAt DESC | ✅ Defined |
146
+ | `builds` | userId + status + uploadedAt DESC | ✅ Defined |
147
+ | `builds` | userId + platform + uploadedAt DESC | ✅ Defined |
148
+ | `builds` | appId + channel + status + uploadedAt DESC | ✅ Defined |
149
+ | `analytics_batch` | appId + windowStart ASC | ✅ Defined |
150
+
151
+ ### 5.2 Plugin Firestore Indexes (`/src/firestore/firestore.indexes.json`)
152
+
153
+ | Collection | Query Pattern | Index Status |
154
+ |------------|---------------|--------------|
155
+ | `manifests` | appId + channel | ✅ Defined |
156
+ | `builds` | appId + channel + uploadedAt DESC | ✅ Defined |
157
+ | `builds` | userId + uploadedAt DESC | ✅ Defined |
158
+ | `builds` | appId + version DESC | ✅ Defined |
159
+ | `deltas` | appId + toVersion | ✅ Defined |
160
+ | `deltas` | appId + fromVersion + toVersion | ✅ Defined |
161
+ | `analytics_batch` | appId + windowStart DESC | ✅ Defined |
162
+ | `analytics_batch` | appId + channel + windowStart DESC | ✅ Defined |
163
+ | `apps` | userId + createdAt DESC | ✅ Defined |
164
+
165
+ ### 5.3 Firebase Rules Coverage
166
+
167
+ | Collection | Rules Location | Permissions |
168
+ |------------|----------------|-------------|
169
+ | `apps` | Website + Plugin | Owner read/write |
170
+ | `builds` | Website + Plugin | Owner read/write |
171
+ | `manifests` | Plugin | Public read, Owner write |
172
+ | `deltas` | Plugin | Public read, Owner write |
173
+ | `users` | Website + Plugin | Owner only |
174
+ | `drive_tokens` | Website + Plugin | Owner only |
175
+ | `analytics` | Website | Write only |
176
+ | `analytics_batch` | Plugin | Public write, Admin read |
177
+
178
+ ---
179
+
180
+ ## 6. Example Applications
181
+
182
+ ### 6.1 React + Capacitor (`/example-apps/react-capacitor/`)
183
+
184
+ | Check | Result | Date |
185
+ |-------|--------|------|
186
+ | `yarn build` | ✅ 0 errors | 2026-01-16 |
187
+ | Vite logLevel | ✅ Set to 'info' | 2026-01-16 |
188
+ | Port configuration | ✅ 5944 (unique) | 2026-01-16 |
189
+
190
+ ### 6.2 Firebase Backend (`/example-apps/firebase-backend/`)
191
+
192
+ | Component | Status |
193
+ |-----------|--------|
194
+ | Cloud Functions | ✅ Complete |
195
+ | Firestore rules | ✅ Complete |
196
+ | Firestore indexes | ✅ Complete |
197
+ | README | ✅ Complete |
198
+
199
+ ### 6.3 Node.js + Express (`/example-apps/node-express/`)
200
+
201
+ | Component | Status |
202
+ |-----------|--------|
203
+ | Express server | ✅ Complete |
204
+ | SQLite database | ✅ Complete |
205
+ | README | ✅ Complete |
206
+
207
+ ---
208
+
209
+ ## 7. Documentation (`/docs/`)
210
+
211
+ | Category | Files | Status |
212
+ |----------|-------|--------|
213
+ | API Reference | 7 files in `/docs/api/` | ✅ Complete |
214
+ | Getting Started | 3 files in `/docs/getting-started/` | ✅ Complete |
215
+ | Features | 3 files in `/docs/features/` | ✅ Complete |
216
+ | Guides | 6 files in `/docs/guides/` | ✅ Complete |
217
+ | Examples | 2 files in `/docs/examples/` | ✅ Complete |
218
+ | Security | 3 files in `/docs/security/` | ✅ Complete |
219
+ | Reports | Multiple files in `/docs/reports/` | ✅ Complete |
220
+ | Plans | Multiple files in `/docs/plans/` | ✅ Complete |
221
+
222
+ ### Key Documentation Files
223
+
224
+ | File | Purpose | Status |
225
+ |------|---------|--------|
226
+ | `ROADMAP.md` | Development roadmap | ✅ Updated 2026-01-16 |
227
+ | `TESTING_REQUIREMENTS.md` | Testing guide | ✅ Created 2026-01-16 |
228
+ | `BACKEND_TEMPLATES_GUIDE.md` | Template customization | ✅ Created 2026-01-16 |
229
+ | `PROJECT_COMPLETION_TRACKER.md` | This file | ✅ Updated 2026-01-16 |
230
+
231
+ ---
232
+
233
+ ## 8. Assets & SVG Verification
234
+
235
+ | Location | Asset Type | Format | Status |
236
+ |----------|------------|--------|--------|
237
+ | `/website/public/favicon.svg` | Favicon | SVG | ✅ |
238
+ | `/website/public/apple-touch-icon.svg` | iOS icon | SVG | ✅ |
239
+ | `/website/public/og-image.svg` | Social share | SVG | ✅ |
240
+ | `/website/public/vite.svg` | Vite logo | SVG | ✅ |
241
+ | `/example-apps/react-capacitor/android/` | App icons | PNG | ✅ (Android requires PNG) |
242
+
243
+ **Note:** Android app icons must be PNG format - this is a platform requirement, not a deviation from SVG policy.
244
+
245
+ ---
246
+
247
+ ## 9. Configuration Files
248
+
249
+ ### 9.1 Vite Configuration
250
+
251
+ | Project | logLevel | Port | strictPort | Status |
252
+ |---------|----------|------|------------|--------|
253
+ | Website | `'info'` | 5942 | true | ✅ Correct |
254
+ | React-Capacitor Example | `'info'` | 5944 | true | ✅ Correct |
255
+
256
+ ### 9.2 ESLint Configuration
257
+
258
+ | Project | @eslint/js usage | TypeScript ESLint | Status |
259
+ |---------|-----------------|-------------------|--------|
260
+ | Main plugin | ❌ Not used | ✅ Used | ✅ Correct |
261
+ | Website | ❌ Not used | ✅ Used | ✅ Correct |
262
+
263
+ ### 9.3 Package Manager
264
+
265
+ | Project | Package Manager | Lockfile | Status |
266
+ |---------|-----------------|----------|--------|
267
+ | Root | yarn@1.22.22 | yarn.lock | ✅ |
268
+ | Website | yarn | yarn.lock | ✅ |
269
+ | CLI | yarn | - | ✅ |
270
+ | Examples | yarn | - | ✅ |
271
+
272
+ ---
273
+
274
+ ## 10. Testing ✅ COMPLETE
275
+
276
+ ### 10.1 TypeScript Unit Tests (`/src/__tests__/`)
277
+
278
+ | Test File | Status |
279
+ |-----------|--------|
280
+ | `bundle-manager.test.ts` | ✅ Complete |
281
+ | `config.test.ts` | ✅ Complete |
282
+ | `delta-processor.test.ts` | ✅ Complete |
283
+ | `firestore-schema.test.ts` | ✅ Complete |
284
+ | `manifest-reader.test.ts` | ✅ Complete |
285
+ | `rollout-checker.test.ts` | ✅ Complete |
286
+ | `security.test.ts` | ✅ Complete |
287
+ | `version-manager.test.ts` | ✅ Complete |
288
+ | `integration.test.ts` | ✅ Complete |
289
+
290
+ ### 10.2 iOS Native Tests (XCTest) ✅ IMPLEMENTED
291
+
292
+ | Test File | Tests | Status |
293
+ |-----------|-------|--------|
294
+ | `SecurityManagerTests.swift` | 6 | ✅ Complete |
295
+ | `LiveUpdateTests.swift` | 10 | ✅ Complete |
296
+ | `AppUpdateTests.swift` | 5 | ✅ Complete |
297
+ | `AppReviewTests.swift` | 5 | ✅ Complete |
298
+ | `BackgroundUpdateTests.swift` | 6 | ✅ Complete |
299
+ | **Total** | **32 tests** | ✅ |
300
+
301
+ **Location:** `/ios/Tests/NativeUpdateTests/`
302
+
303
+ ### 10.3 Android Native Tests (JUnit/Kotlin) ✅ IMPLEMENTED
304
+
305
+ | Test File | Tests | Status |
306
+ |-----------|-------|--------|
307
+ | `SecurityManagerTest.kt` | 10 | ✅ Complete |
308
+ | `LiveUpdatePluginTest.kt` | 10 | ✅ Complete |
309
+ | `AppUpdatePluginTest.kt` | 5 | ✅ Complete |
310
+ | `AppReviewPluginTest.kt` | 5 | ✅ Complete |
311
+ | `BackgroundUpdateWorkerTest.kt` | 5 | ✅ Complete |
312
+ | `BackgroundNotificationManagerTest.kt` | 5 | ✅ Complete |
313
+ | **Total** | **40 tests** | ✅ |
314
+
315
+ **Location:** `/android/src/test/java/com/aoneahsan/nativeupdate/`
316
+
317
+ ### 10.4 E2E Tests (Detox) ✅ IMPLEMENTED
318
+
319
+ | Spec File | Tests | Status |
320
+ |-----------|-------|--------|
321
+ | `ota-update.e2e.spec.js` | 5 | ✅ Complete |
322
+ | `download-progress.e2e.spec.js` | 5 | ✅ Complete |
323
+ | `channel-switching.e2e.spec.js` | 5 | ✅ Complete |
324
+ | `error-handling.e2e.spec.js` | 4 | ✅ Complete |
325
+ | **Total** | **19 tests** | ✅ |
326
+
327
+ **Location:** `/e2e/specs/`
328
+
329
+ ### 10.5 Test Summary
330
+
331
+ | Category | Files | Tests | Status |
332
+ |----------|-------|-------|--------|
333
+ | TypeScript Unit Tests | 9 | ~30 | ✅ Complete |
334
+ | iOS Native Tests | 5 | 32 | ✅ Complete |
335
+ | Android Native Tests | 6 | 40 | ✅ Complete |
336
+ | E2E Tests | 4 | 19 | ✅ Complete |
337
+ | **Total** | **24** | **~121** | ✅ |
338
+
339
+ ---
340
+
341
+ ## 11. Known Items & Notes
342
+
343
+ ### 11.1 Backend Template TODOs (Intentional)
344
+
345
+ The TODOs in `/cli/commands/backend-create.js` are **intentional** - they are inside template strings that get written to user-generated backend files. These are documented customization points.
346
+
347
+ **Documentation:** `/docs/guides/BACKEND_TEMPLATES_GUIDE.md`
348
+
349
+ ### 11.2 Delta Processor WASM (By Design)
350
+
351
+ The delta processor (`/src/live-update/delta-processor.ts`) has a WASM placeholder with proper fallback to JavaScript implementation. This is by design for future optimization.
352
+
353
+ ### 11.3 Native Tests ✅ IMPLEMENTED
354
+
355
+ Native platform tests are fully implemented:
356
+ - **iOS XCTest:** 5 test files with 32 tests in `/ios/Tests/NativeUpdateTests/`
357
+ - **Android JUnit/Kotlin:** 6 test files with 40 tests in `/android/src/test/`
358
+ - **E2E Detox:** 4 spec files with 19 tests in `/e2e/specs/`
359
+
360
+ See `/docs/TESTING_REQUIREMENTS.md` for detailed test documentation.
361
+
362
+ ---
363
+
364
+ ## 12. Deployment Readiness Checklist
365
+
366
+ - [x] All source files pass lint (0 warnings)
367
+ - [x] All builds pass (0 errors)
368
+ - [x] Firebase rules defined for all collections
369
+ - [x] Firebase indexes match all Firestore queries
370
+ - [x] All Vite configs use logLevel: 'info'
371
+ - [x] All web assets are SVG format
372
+ - [x] Documentation complete and up-to-date
373
+ - [x] Example apps functional and build successfully
374
+ - [x] CLI tools operational (8 commands)
375
+ - [x] No hardcoded secrets in codebase
376
+ - [x] Environment variables documented
377
+ - [x] CLAUDE.md updated with testing policy
378
+ - [x] Unique dev server ports configured
379
+
380
+ ---
381
+
382
+ ## 13. Future Enhancements (Optional)
383
+
384
+ Documented in `/docs/ROADMAP.md` as optional future phases:
385
+
386
+ ### ~~Phase 1: Advanced Testing~~ ✅ COMPLETE
387
+ - ✅ iOS XCTest implementation - 5 files, 32 tests
388
+ - ✅ Android JUnit tests - 6 files, 40 tests
389
+ - ✅ E2E test suite with Detox - 4 specs, 19 tests
390
+
391
+ ### Phase 2: Enterprise Features
392
+ - Delta updates WASM optimization
393
+ - Multi-tenant SaaS platform
394
+ - Enterprise SSO integration
395
+ - Advanced rollout strategies
396
+
397
+ ### Phase 3: Community Features
398
+ - Video tutorials
399
+ - Additional framework examples (Vue, Angular)
400
+ - CI/CD integration templates
401
+
402
+ ---
403
+
404
+ ## 14. Verification Commands
405
+
406
+ ```bash
407
+ # Main plugin
408
+ cd /home/ahsan/Documents/01-code/01-packages/native-update
409
+ yarn lint # Should show 0 warnings
410
+ yarn build # Should show 0 errors
411
+
412
+ # Website
413
+ cd website
414
+ yarn lint # Should show 0 warnings
415
+ yarn build # Should show 0 errors
416
+
417
+ # React-Capacitor Example
418
+ cd example-apps/react-capacitor
419
+ yarn build # Should show 0 errors
420
+
421
+ # CLI
422
+ node cli/index.js --help # Should show all 8 commands
423
+
424
+ # Firebase indexes deployment
425
+ firebase deploy --only firestore:indexes
426
+ ```
427
+
428
+ ---
429
+
430
+ ## 15. Contact & Support
431
+
432
+ **Developer:** Ahsan Mahmood
433
+ **Email:** aoneahsan@gmail.com
434
+ **Website:** https://nativeupdate.aoneahsan.com
435
+ **NPM:** https://www.npmjs.com/package/native-update
436
+ **GitHub:** https://github.com/aoneahsan
437
+
438
+ ---
439
+
440
+ **Document maintained by:** Development Team
441
+ **Review frequency:** Before each release
442
+ **Last comprehensive review:** 2026-01-16
@@ -10,7 +10,7 @@ Get up and running with Capacitor Native Update in 5 minutes! This guide covers
10
10
 
11
11
  ```bash
12
12
  # Install the plugin
13
- npm install native-update
13
+ yarn add native-update
14
14
 
15
15
  # Sync with native projects
16
16
  npx cap sync
@@ -150,8 +150,8 @@ Use our example server or implement your own:
150
150
  ```bash
151
151
  # Using example server
152
152
  cd server-example
153
- npm install
154
- npm start
153
+ yarn install
154
+ yarn start
155
155
 
156
156
  # Upload a bundle
157
157
  curl -X POST http://localhost:3000/api/v1/bundles \
@@ -504,7 +504,7 @@ export class AppComponent implements OnInit {
504
504
 
505
505
  ```bash
506
506
  # Build your web app
507
- npm run build
507
+ yarn build
508
508
 
509
509
  # Create update bundle
510
510
  cd www && zip -r ../update-1.0.1.zip . && cd ..
package/docs/ROADMAP.md CHANGED
@@ -68,9 +68,9 @@ The plugin provides complete OTA update functionality with native implementation
68
68
  - [x] Security module (2 files)
69
69
  - [x] Firestore integration (4 files)
70
70
 
71
- ### 4. Testing Suite PARTIAL
71
+ ### 4. Testing Suite COMPLETE
72
72
 
73
- #### Unit Tests
73
+ #### TypeScript Unit Tests
74
74
  - [x] Bundle manager tests
75
75
  - [x] Config tests
76
76
  - [x] Delta processor tests
@@ -80,13 +80,27 @@ The plugin provides complete OTA update functionality with native implementation
80
80
  - [x] Security tests
81
81
  - [x] Version manager tests
82
82
  - [x] Integration tests
83
- - [ ] iOS native tests (Not implemented)
84
- - [ ] Android native tests (Not implemented)
85
83
 
86
- #### E2E Tests
87
- - [ ] Complete update lifecycle
88
- - [ ] Multi-version updates
89
- - [ ] Security validation
84
+ #### iOS Native Tests (XCTest) ✅ IMPLEMENTED
85
+ - [x] SecurityManagerTests.swift (6 tests)
86
+ - [x] LiveUpdateTests.swift (10 tests)
87
+ - [x] AppUpdateTests.swift (5 tests)
88
+ - [x] AppReviewTests.swift (5 tests)
89
+ - [x] BackgroundUpdateTests.swift (6 tests)
90
+
91
+ #### Android Native Tests (JUnit/Kotlin) ✅ IMPLEMENTED
92
+ - [x] SecurityManagerTest.kt (10 tests)
93
+ - [x] LiveUpdatePluginTest.kt (10 tests)
94
+ - [x] AppUpdatePluginTest.kt (5 tests)
95
+ - [x] AppReviewPluginTest.kt (5 tests)
96
+ - [x] BackgroundUpdateWorkerTest.kt (5 tests)
97
+ - [x] BackgroundNotificationManagerTest.kt (5 tests)
98
+
99
+ #### E2E Tests (Detox) ✅ IMPLEMENTED
100
+ - [x] Complete update lifecycle (ota-update.e2e.spec.js)
101
+ - [x] Download progress tracking (download-progress.e2e.spec.js)
102
+ - [x] Channel switching (channel-switching.e2e.spec.js)
103
+ - [x] Error handling (error-handling.e2e.spec.js)
90
104
 
91
105
  ### 5. Tooling and Utilities ✅ COMPLETE
92
106
 
@@ -138,10 +152,10 @@ The plugin provides complete OTA update functionality with native implementation
138
152
 
139
153
  ## 🎯 Future Enhancement Phases
140
154
 
141
- ### Phase 1: Advanced Testing (Optional)
142
- - [ ] iOS XCTest implementation
143
- - [ ] Android JUnit tests
144
- - [ ] E2E test suite with Detox/Appium
155
+ ### ~~Phase 1: Advanced Testing~~ ✅ COMPLETE (2026-01-16)
156
+ - [x] iOS XCTest implementation - 5 files, 32 tests
157
+ - [x] Android JUnit tests - 6 files, 40 tests
158
+ - [x] E2E test suite with Detox - 4 specs, 19 tests
145
159
 
146
160
  ### Phase 2: Enterprise Features (Optional)
147
161
  - [ ] Delta updates WASM optimization
@@ -170,30 +184,32 @@ The plugin provides complete OTA update functionality with native implementation
170
184
  | Documentation | ✅ Complete | 100% |
171
185
  | Marketing Website | ✅ Complete | 100% |
172
186
  | Example Applications | ✅ Complete | 100% |
173
- | Unit Tests | Partial | 80% |
174
- | Native Tests | Pending | 0% |
175
- | E2E Tests | Pending | 0% |
187
+ | TypeScript Unit Tests | Complete | 100% |
188
+ | iOS Native Tests (XCTest) | Complete | 100% |
189
+ | Android Native Tests (JUnit) | Complete | 100% |
190
+ | E2E Tests (Detox) | ✅ Complete | 100% |
176
191
 
177
- **Overall Status:** Production Ready for Core Functionality
192
+ **Overall Status:** Production Ready with Comprehensive Test Coverage
178
193
 
179
194
  ---
180
195
 
181
196
  ## 📝 Notes
182
197
 
183
198
  - Core plugin functionality is complete and tested
184
- - Native tests are optional but recommended for production deployments
185
- - Enterprise features can be added based on user requirements
199
+ - Native tests are fully implemented (iOS XCTest, Android JUnit)
200
+ - E2E tests are implemented with Detox framework
186
201
  - All builds pass with zero errors
187
202
  - All lint checks pass with zero warnings
203
+ - Comprehensive test coverage with ~121 tests across all categories
188
204
 
189
205
  ---
190
206
 
191
207
  ## 🤝 Contributing
192
208
 
193
209
  We welcome contributions! Focus areas:
194
- - Native platform testing
195
- - E2E testing frameworks
196
210
  - Additional backend examples
197
211
  - Framework-specific adapters
212
+ - Enterprise feature development
213
+ - Delta updates optimization
198
214
 
199
215
  See [CONTRIBUTING.md](../CONTRIBUTING.md) for guidelines.
@@ -1,12 +1,15 @@
1
1
  # Testing Requirements
2
2
 
3
- This document outlines the testing requirements for the native-update plugin. Tests should be implemented when explicitly requested by the user.
3
+ This document outlines the testing requirements for the native-update plugin.
4
4
 
5
5
  ## Current Testing Status
6
6
 
7
7
  - **Plugin testing**: Handled in actual/live applications using the plugin
8
8
  - **Example apps**: Demonstrate functionality for manual testing
9
- - **Unit tests**: Not yet implemented (documented here for future reference)
9
+ - **Unit tests (TypeScript)**: Implemented in `/src/__tests__/`
10
+ - **iOS Native Tests (XCTest)**: ✅ Implemented in `/ios/Tests/NativeUpdateTests/`
11
+ - **Android Native Tests (JUnit/Kotlin)**: ✅ Implemented in `/android/src/test/`
12
+ - **E2E Tests (Detox)**: ✅ Implemented in `/e2e/`
10
13
 
11
14
  ---
12
15
 
@@ -59,9 +62,20 @@ ios/
59
62
  ### Running iOS Tests
60
63
  ```bash
61
64
  cd ios
62
- xcodebuild test -scheme NativeUpdate -destination 'platform=iOS Simulator,name=iPhone 14'
65
+ xcodebuild test -scheme NativeUpdate -destination 'platform=iOS Simulator,name=iPhone 15'
63
66
  ```
64
67
 
68
+ ### Current Test Files (✅ Implemented)
69
+
70
+ | File | Tests | Status |
71
+ |------|-------|--------|
72
+ | `SecurityManagerTests.swift` | 6 tests | ✅ |
73
+ | `LiveUpdateTests.swift` | 10 tests | ✅ |
74
+ | `AppUpdateTests.swift` | 5 tests | ✅ |
75
+ | `AppReviewTests.swift` | 5 tests | ✅ |
76
+ | `BackgroundUpdateTests.swift` | 6 tests | ✅ |
77
+ | **Total** | **32 tests** | ✅ |
78
+
65
79
  ---
66
80
 
67
81
  ## Android Native Tests (JUnit)
@@ -123,13 +137,63 @@ cd android
123
137
  ./gradlew connectedAndroidTest # For instrumented tests
124
138
  ```
125
139
 
140
+ ### Current Test Files (✅ Implemented)
141
+
142
+ | File | Tests | Status |
143
+ |------|-------|--------|
144
+ | `SecurityManagerTest.kt` | 10 tests | ✅ |
145
+ | `LiveUpdatePluginTest.kt` | 10 tests | ✅ |
146
+ | `AppUpdatePluginTest.kt` | 5 tests | ✅ |
147
+ | `AppReviewPluginTest.kt` | 5 tests | ✅ |
148
+ | `BackgroundUpdateWorkerTest.kt` | 5 tests | ✅ |
149
+ | `BackgroundNotificationManagerTest.kt` | 5 tests | ✅ |
150
+ | **Total** | **40 tests** | ✅ |
151
+
152
+ ### Test Dependencies (Added to build.gradle)
153
+
154
+ ```gradle
155
+ testImplementation 'junit:junit:4.13.2'
156
+ testImplementation 'org.mockito:mockito-core:5.8.0'
157
+ testImplementation 'org.mockito.kotlin:mockito-kotlin:5.2.1'
158
+ testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
159
+ testImplementation 'com.google.truth:truth:1.1.5'
160
+ testImplementation 'org.robolectric:robolectric:4.11.1'
161
+ ```
162
+
126
163
  ---
127
164
 
128
- ## End-to-End Tests (Detox/Appium)
165
+ ## End-to-End Tests (Detox) ✅ IMPLEMENTED
166
+
167
+ The E2E tests are implemented in `/e2e/` using Detox framework.
129
168
 
130
- ### Recommended Framework
169
+ ### Framework
131
170
  - **Detox** for React Native + Capacitor apps
132
- - **Appium** for platform-agnostic testing
171
+
172
+ ### Test Coverage
173
+
174
+ | Spec File | Tests | Status |
175
+ |-----------|-------|--------|
176
+ | `ota-update.e2e.spec.js` | 5 tests | ✅ |
177
+ | `download-progress.e2e.spec.js` | 5 tests | ✅ |
178
+ | `channel-switching.e2e.spec.js` | 5 tests | ✅ |
179
+ | `error-handling.e2e.spec.js` | 4 tests | ✅ |
180
+ | **Total** | **19 tests** | ✅ |
181
+
182
+ ### Running E2E Tests
183
+
184
+ ```bash
185
+ cd e2e
186
+ yarn install
187
+ yarn build:android # or yarn build:ios
188
+ yarn test:android # or yarn test:ios
189
+ ```
190
+
191
+ ### Mock Server
192
+
193
+ Start the mock update server for E2E tests:
194
+ ```bash
195
+ yarn start-mock-server
196
+ ```
133
197
 
134
198
  ### Test Scenarios
135
199
 
@@ -212,15 +276,37 @@ The CLI already provides `npx native-update server start` for local testing.
212
276
 
213
277
  ---
214
278
 
215
- ## When to Implement Tests
279
+ ## Test Implementation Status
280
+
281
+ All test categories have been implemented as of 2026-01-16:
282
+
283
+ | Category | Location | Tests | Status |
284
+ |----------|----------|-------|--------|
285
+ | TypeScript Unit Tests | `/src/__tests__/` | 9 test files | ✅ Complete |
286
+ | iOS Native Tests | `/ios/Tests/NativeUpdateTests/` | 5 test files, 32 tests | ✅ Complete |
287
+ | Android Native Tests | `/android/src/test/.../nativeupdate/` | 6 test files, 40 tests | ✅ Complete |
288
+ | E2E Tests | `/e2e/specs/` | 4 spec files, 19 tests | ✅ Complete |
289
+ | **Total** | | **~100 tests** | ✅ Complete |
290
+
291
+ ## Running All Tests
292
+
293
+ ```bash
294
+ # TypeScript Unit Tests
295
+ yarn test
296
+
297
+ # iOS Native Tests
298
+ cd ios && xcodebuild test -scheme NativeUpdate -destination 'platform=iOS Simulator,name=iPhone 15'
299
+
300
+ # Android Native Tests
301
+ cd android && ./gradlew test
302
+
303
+ # E2E Tests
304
+ cd e2e && yarn install && yarn test:android # or yarn test:ios
305
+ ```
216
306
 
217
- Tests should be implemented when:
218
- 1. User explicitly requests test implementation
219
- 2. Preparing for npm publication
220
- 3. Before major version releases
221
- 4. When contributing to the project
307
+ ## Additional Testing
222
308
 
223
- Until then, testing is handled through:
224
- - Manual testing in example apps
225
- - Integration testing in actual production apps
309
+ Manual testing is also supported through:
310
+ - Example apps for end-user testing
226
311
  - CLI command verification
312
+ - Integration testing in production apps
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "native-update",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "Foundation package for building a comprehensive update system for Capacitor apps. Provides architecture and interfaces but requires backend implementation.",
5
5
  "type": "module",
6
6
  "main": "dist/plugin.cjs.js",