UNPKG

native-update

Version:

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

696 lines (577 loc) 17.4 kB
# Native App Updates Implementation Guide This comprehensive guide explains how to implement Native App Updates (App Store and Google Play updates) in your Capacitor application using the NativeUpdate plugin. ## Table of Contents - [Overview](#overview) - [Platform Differences](#platform-differences) - [Setup Guide](#setup-guide) - [Implementation Steps](#implementation-steps) - [UI/UX Best Practices](#uiux-best-practices) - [Testing](#testing) - [Troubleshooting](#troubleshooting) ## Overview Native App Updates allow your app to: - 🔄 Check for new versions in app stores - 📥 Download updates within the app - 🚀 Install updates seamlessly - 📊 Track update adoption rates ### Benefits - **User Experience**: Users update without leaving your app - **Adoption Rate**: Higher update rates vs waiting for manual updates - **Control**: Guide users through important updates - **Flexibility**: Immediate vs flexible update strategies ## Platform Differences ### Android (Google Play) - Uses Google Play Core Library - Supports In-App Updates API - Two update modes: Immediate and Flexible - Requires Google Play Store (not available on other stores) ### iOS (App Store) - Uses StoreKit framework - Redirects to App Store for download - Cannot install directly within app - Shows update availability only ## Setup Guide ### Prerequisites 1. **Android**: - App must be published on Google Play Store - Minimum API level 21 (Android 5.0) - Google Play Core library 2. **iOS**: - App must be published on Apple App Store - iOS 10.0 or higher - Valid App Store ID ### Installation ```bash npm install native-update npx cap sync ``` ### Android Configuration 1. **Add to `android/app/build.gradle`:** ```gradle dependencies { implementation 'com.google.android.play:core:2.1.0' implementation 'com.google.android.play:core-ktx:1.8.1' } ``` 2. **Add to `AndroidManifest.xml`:** ```xml <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> ``` ### iOS Configuration 1. **Add to `Info.plist`:** ```xml <key>LSApplicationQueriesSchemes</key> <array> <string>itms-apps</string> </array> ``` 2. **Enable Capabilities in Xcode:** - App Groups (for shared data) - Background Modes > Background fetch ## Implementation Steps ### Step 1: Basic Implementation ```typescript import { NativeUpdate } from 'native-update'; import { Capacitor } from '@capacitor/core'; export class NativeUpdateService { async checkForAppUpdate() { try { // Check current platform const platform = Capacitor.getPlatform(); // Check for native app updates const result = await NativeUpdate.getAppUpdateInfo(); if (result.updateAvailable) { console.log(`Update available: ${result.availableVersion}`); console.log(`Current version: ${result.currentVersion}`); // Handle based on update type if (result.immediateUpdateAllowed) { await this.performImmediateUpdate(); } else if (result.flexibleUpdateAllowed) { await this.performFlexibleUpdate(); } else if (platform === 'ios') { await this.redirectToAppStore(); } } } catch (error) { console.error('Update check failed:', error); } } } ``` ### Step 2: Android In-App Updates #### Immediate Update (Blocking) ```typescript export class AndroidImmediateUpdate { async performImmediateUpdate() { try { // Start immediate update const { started } = await NativeUpdate.performImmediateUpdate(); if (started) { // The app will be restarted automatically after update console.log('Immediate update started'); } } catch (error) { if (error.code === 'UPDATE_CANCELED') { // User canceled the update // For immediate updates, you might want to close the app await this.showMandatoryUpdateDialog(); } } } async showMandatoryUpdateDialog() { const alert = await this.alertController.create({ header: 'Update Required', message: 'This update is required to continue using the app.', backdropDismiss: false, buttons: [ { text: 'Update', handler: () => { this.performImmediateUpdate(); }, }, ], }); await alert.present(); } } ``` #### Flexible Update (Non-blocking) ```typescript export class AndroidFlexibleUpdate { private updateDownloaded = false; async performFlexibleUpdate() { try { // Start flexible update await NativeUpdate.startFlexibleUpdate(); // Listen for download progress NativeUpdate.addListener( 'appUpdateProgress', (progress) => { console.log( `Download progress: ${progress.bytesDownloaded} / ${progress.totalBytesToDownload}` ); this.updateProgressUI(progress); } ); // Listen for download completion NativeUpdate.addListener('updateStateChanged', (event) => { if (event.status === 'DOWNLOADED') { console.log('Update downloaded'); this.updateDownloaded = true; this.showInstallPrompt(); } }); } catch (error) { console.error('Flexible update failed:', error); } } async showInstallPrompt() { const alert = await this.alertController.create({ header: 'Update Ready', message: 'A new version has been downloaded. Would you like to install it now?', buttons: [ { text: 'Later', role: 'cancel', handler: () => { // Install will happen on next app restart }, }, { text: 'Install', handler: () => { this.completeFlexibleUpdate(); }, }, ], }); await alert.present(); } async completeFlexibleUpdate() { try { await NativeUpdate.completeFlexibleUpdate(); // App will restart with new version } catch (error) { console.error('Failed to complete update:', error); } } updateProgressUI(progress: any) { const percent = Math.round( (progress.bytesDownloaded / progress.totalBytesToDownload) * 100 ); // Update your UI progress bar this.downloadProgress = percent; } } ``` ### Step 3: iOS App Store Updates ```typescript export class iOSAppStoreUpdate { async checkAndPromptUpdate() { try { const result = await NativeUpdate.getAppUpdateInfo(); if (result.updateAvailable) { await this.showiOSUpdateDialog(result); } } catch (error) { console.error('iOS update check failed:', error); } } async showiOSUpdateDialog(updateInfo: any) { const alert = await this.alertController.create({ header: 'Update Available', message: `Version ${updateInfo.availableVersion} is available on the App Store.`, buttons: [ { text: 'Later', role: 'cancel', }, { text: 'Update', handler: () => { this.openAppStore(); }, }, ], }); await alert.present(); } async openAppStore() { try { await NativeUpdate.openAppStore(); } catch (error) { // Fallback to browser const appStoreUrl = `https://apps.apple.com/app/id${YOUR_APP_STORE_ID}`; window.open(appStoreUrl, '_system'); } } } ``` ### Step 4: Cross-Platform Implementation ```typescript import { Capacitor } from '@capacitor/core'; import { NativeUpdate } from 'native-update'; export class UnifiedUpdateService { private platform = Capacitor.getPlatform(); async checkAndUpdateApp() { try { const updateInfo = await NativeUpdate.getAppUpdateInfo(); if (!updateInfo.updateAvailable) { console.log('App is up to date'); return; } // Platform-specific handling if (this.platform === 'android') { await this.handleAndroidUpdate(updateInfo); } else if (this.platform === 'ios') { await this.handleiOSUpdate(updateInfo); } } catch (error) { console.error('Update check failed:', error); this.handleUpdateError(error); } } private async handleAndroidUpdate(updateInfo: any) { // Determine update priority const priority = updateInfo.updatePriority || 0; if (priority >= 4 || updateInfo.immediateUpdateAllowed) { // High priority or immediate update required await this.showUpdateDialog({ title: 'Important Update', message: 'A critical update is available and must be installed.', mandatory: true, action: () => this.performImmediateUpdate(), }); } else if (updateInfo.flexibleUpdateAllowed) { // Normal priority - flexible update await this.showUpdateDialog({ title: 'Update Available', message: `Version ${updateInfo.availableVersion} is available with new features and improvements.`, mandatory: false, action: () => this.performFlexibleUpdate(), }); } } private async handleiOSUpdate(updateInfo: any) { await this.showUpdateDialog({ title: 'Update Available', message: `Version ${updateInfo.availableVersion} is available on the App Store.`, mandatory: false, action: () => NativeUpdate.openAppStore(), }); } private async showUpdateDialog(options: { title: string; message: string; mandatory: boolean; action: () => Promise<void>; }) { const buttons = options.mandatory ? [ { text: 'Update Now', handler: () => options.action(), }, ] : [ { text: 'Later', role: 'cancel', }, { text: 'Update', handler: () => options.action(), }, ]; const alert = await this.alertController.create({ header: options.title, message: options.message, backdropDismiss: !options.mandatory, buttons, }); await alert.present(); } } ``` ### Step 5: Update Status Handling ```typescript export class UpdateStatusManager { constructor() { this.setupUpdateListeners(); } private setupUpdateListeners() { // Installation status NativeUpdate.addListener('updateStateChanged', (status) => { switch (status.status) { case 'PENDING': console.log('Update pending'); break; case 'DOWNLOADING': console.log('Downloading update'); break; case 'INSTALLING': console.log('Installing update'); break; case 'INSTALLED': console.log('Update installed'); this.notifyUpdateComplete(); break; case 'FAILED': console.error('Update failed:', status.error); this.handleUpdateFailure(status.error); break; case 'CANCELED': console.log('Update canceled by user'); break; } }); // Download progress (Android flexible updates) NativeUpdate.addListener( 'onAppUpdateDownloadProgress', (progress) => { const percent = Math.round( (progress.bytesDownloaded / progress.totalBytesToDownload) * 100 ); this.updateProgressBar(percent); } ); } private updateProgressBar(percent: number) { // Update your UI const progressBar = document.querySelector('.update-progress'); if (progressBar) { progressBar.setAttribute('value', percent.toString()); } } } ``` ## UI/UX Best Practices ### 1. Update Prompts ```typescript export class UpdateUIService { async showSmartUpdatePrompt(updateInfo: any) { const timeSinceLastPrompt = Date.now() - this.lastPromptTime; const oneDayInMs = 24 * 60 * 60 * 1000; // Don't prompt too frequently if (timeSinceLastPrompt < oneDayInMs && !updateInfo.mandatory) { return; } // Custom UI based on update importance if (updateInfo.updatePriority >= 4) { await this.showCriticalUpdateUI(updateInfo); } else if (updateInfo.releaseNotes?.includes('security')) { await this.showSecurityUpdateUI(updateInfo); } else { await this.showStandardUpdateUI(updateInfo); } this.lastPromptTime = Date.now(); } private async showCriticalUpdateUI(updateInfo: any) { // Full-screen modal for critical updates const modal = await this.modalController.create({ component: CriticalUpdateComponent, componentProps: { updateInfo }, backdropDismiss: false, }); await modal.present(); } private async showStandardUpdateUI(updateInfo: any) { // Toast or small banner for standard updates const toast = await this.toastController.create({ message: 'A new version is available', duration: 5000, position: 'top', buttons: [ { text: 'Update', handler: () => this.startUpdate(), }, ], }); await toast.present(); } } ``` ### 2. Progress Indicators ```html <!-- update-progress.component.html --> <ion-card *ngIf="updateInProgress"> <ion-card-header> <ion-card-title>Updating App</ion-card-title> </ion-card-header> <ion-card-content> <ion-progress-bar [value]="downloadProgress / 100"></ion-progress-bar> <p>{{ downloadProgress }}% complete</p> <p class="update-status">{{ updateStatus }}</p> </ion-card-content> </ion-card> ``` ### 3. Smart Update Scheduling ```typescript export class SmartUpdateScheduler { async scheduleUpdate() { const updateInfo = await NativeUpdate.getAppUpdateInfo(); if (!updateInfo.updateAvailable) return; // Check user preferences const preferences = await this.getUpdatePreferences(); if (preferences.autoUpdate === 'wifi-only') { const connection = await Network.getStatus(); if (connection.connectionType !== 'wifi') { // Schedule for later when on WiFi this.scheduleForWiFi(); return; } } if (preferences.autoUpdate === 'night-only') { const hour = new Date().getHours(); if (hour >= 6 && hour <= 22) { // Schedule for nighttime this.scheduleForNight(); return; } } // Proceed with update await this.performUpdate(); } } ``` ## Testing ### Android Testing 1. **Using Internal Test Track**: ```typescript // Enable internal app sharing for testing await NativeUpdate.enableDebugMode({ enabled: true, testMode: 'internal-test', }); ``` 2. **Test Different Scenarios**: ```typescript // Test immediate update await NativeUpdate.simulateUpdate({ type: 'immediate', version: '2.0.0', }); // Test flexible update await NativeUpdate.simulateUpdate({ type: 'flexible', version: '1.1.0', }); ``` ### iOS Testing 1. **TestFlight Testing**: - Upload build to TestFlight - Install older version on device - Check for updates in app 2. **Sandbox Testing**: - Use sandbox App Store account - Test update flow without affecting production ### Testing Checklist - [ ] Update available detection - [ ] Update not available scenario - [ ] Immediate update flow (Android) - [ ] Flexible update flow (Android) - [ ] App Store redirect (iOS) - [ ] Update cancellation handling - [ ] Network error handling - [ ] Update completion verification - [ ] Rollback scenarios (if applicable) ## Troubleshooting ### Common Issues #### 1. Update Not Detected ```typescript // Debug update detection const debug = await NativeUpdate.getDebugInfo(); console.log('Debug info:', { currentVersion: debug.currentVersion, packageName: debug.packageName, lastCheckTime: debug.lastCheckTime, playServicesAvailable: debug.playServicesAvailable, // Android }); ``` #### 2. Update Fails to Install ```typescript // Check update state const state = await NativeUpdate.getUpdateState(); if (state.status === 'FAILED') { // Clear update data and retry await NativeUpdate.clearUpdateData(); await NativeUpdate.getAppUpdateInfo(); } ``` #### 3. iOS App Store Not Opening ```typescript // App Store ID is configured in capacitor.config.json // Check your configuration file for the appStoreId setting // Manual fallback if (!config.appStoreId) { console.error('App Store ID not configured'); } ``` ### Platform-Specific Issues #### Android - Ensure Google Play Services is updated - Check Play Store app is installed - Verify app is signed with release key - Test on device with Play Store (not emulator) #### iOS - Verify App Store ID is correct - Check iTunes lookup API response - Ensure app is live on App Store - Test on real device (not simulator) ## Best Practices Summary 1. **Always provide "Later" option** for non-critical updates 2. **Show release notes** when available 3. **Respect user preferences** for update timing 4. **Handle network conditions** appropriately 5. **Test thoroughly** on both platforms 6. **Monitor update metrics** to improve adoption 7. **Provide fallback options** for update failures ## Next Steps - Learn about [Live Updates (OTA)](./LIVE_UPDATES_GUIDE.md) - Implement [App Review Features](./APP_REVIEW_GUIDE.md) - Review [Security Guidelines](./guides/security-best-practices.md) - Check [API Reference](./api/app-update-api.md)