Примеры Разработки Гибридных Приложений Capacitor

Примеры разработки гибридных приложений Capacitor включая нативные API устройств, плагины, кроссплатформенное развертывание и возможности Progressive Web App

💻 Нативные API Устройств Capacitor typescript

🟢 simple

Реализовать нативные функции устройства используя плагины Capacitor включая камеру, геолокацию, уведомления и доступ к файловой системе

// capacitor.config.ts
import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.app',
  appName: 'Device API Demo',
  webDir: 'dist',
  bundledWebRuntime: false,
  plugins: {
    Camera: {
      permissions: ['camera', 'photos']
    },
    Geolocation: {
      permissions: ['location']
    },
    PushNotifications: {
      presentationOptions: ['badge', 'sound', 'alert']
    },
    LocalNotifications: {
      smallIcon: 'ic_stat_icon_config_sample',
      iconColor: '#488AFF',
      sound: 'beep.wav'
    },
    SplashScreen: {
      launchShowDuration: 3000,
      launchAutoHide: true,
      backgroundColor: '#3880ff',
      androidSplashResourceName: 'splash',
      androidScaleType: 'CENTER_CROP',
      showSpinner: true,
      androidSpinnerStyle: 'large',
      iosSpinnerStyle: 'small',
      spinnerColor: '#999999',
      splashFullScreen: true,
      splashImmersive: true,
      layoutName: 'launch_screen',
      useDialog: true
    }
  }
};

export default config;

// src/services/DeviceService.ts
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
import { Geolocation, Position } from '@capacitor/geolocation';
import { LocalNotifications, LocalNotificationSchema } from '@capacitor/local-notifications';
import { PushNotifications, PushNotificationSchema, PermissionStatus } from '@capacitor/push-notifications';
import { Filesystem, Directory } from '@capacitor/filesystem';
import { Share } from 'capacitor-share';

export interface DevicePosition {
  latitude: number;
  longitude: number;
  accuracy: number;
  timestamp: number;
}

export interface CameraPhoto {
  filepath: string;
  webviewPath?: string;
  base64?: string;
}

export class DeviceService {
  // Camera Operations
  static async takePhoto(): Promise<CameraPhoto> {
    try {
      const photo: Photo = await Camera.getPhoto({
        resultType: CameraResultType.Uri,
        source: CameraSource.Camera,
        quality: 90,
        allowEditing: true,
        promptLabelHeader: 'Take a Photo',
        promptLabelCancel: 'Cancel',
        promptLabelPhoto: 'From Gallery',
        promptLabelPicture: 'Take Photo'
      });

      return {
        filepath: photo.path || '',
        webviewPath: photo.webPath
      };
    } catch (error) {
      console.error('Camera error:', error);
      throw new Error('Failed to take photo');
    }
  }

  static async selectFromGallery(): Promise<CameraPhoto> {
    try {
      const photo: Photo = await Camera.getPhoto({
        resultType: CameraResultType.Uri,
        source: CameraSource.Photos,
        quality: 90
      });

      return {
        filepath: photo.path || '',
        webviewPath: photo.webPath
      };
    } catch (error) {
      console.error('Gallery error:', error);
      throw new Error('Failed to select photo from gallery');
    }
  }

  // Geolocation Operations
  static async getCurrentPosition(): Promise<DevicePosition> {
    try {
      const coordinates: Position = await Geolocation.getCurrentPosition({
        enableHighAccuracy: true,
        timeout: 10000,
        maximumAge: 0
      });

      return {
        latitude: coordinates.coords.latitude,
        longitude: coordinates.coords.longitude,
        accuracy: coordinates.coords.accuracy,
        timestamp: coordinates.timestamp
      };
    } catch (error) {
      console.error('Geolocation error:', error);
      throw new Error('Failed to get current position');
    }
  }

  static watchPosition(
    callback: (position: DevicePosition) => void
  ): Promise<string> {
    return Geolocation.watchPosition(
      {
        enableHighAccuracy: true,
        timeout: 10000,
        maximumAge: 0
      },
      (position, err) => {
        if (err) {
          console.error('Position watch error:', err);
          return;
        }

        if (position) {
          callback({
            latitude: position.coords.latitude,
            longitude: position.coords.longitude,
            accuracy: position.coords.accuracy,
            timestamp: position.timestamp
          });
        }
      }
    );
  }

  // Notification Operations
  static async requestNotificationPermission(): Promise<boolean> {
    try {
      const permStatus: PermissionStatus = await PushNotifications.requestPermissions();
      return permStatus.receive === 'granted';
    } catch (error) {
      console.error('Notification permission error:', error);
      return false;
    }
  }

  static async scheduleLocalNotification(
    title: string,
    body: string,
    schedule?: { at: Date },
    id: number = Date.now()
  ): Promise<void> {
    try {
      const notification: LocalNotificationSchema = {
        id,
        title,
        body,
        schedule,
        sound: 'beep.wav',
        smallIcon: 'ic_stat_icon_config_sample',
        iconColor: '#488AFF',
        actionTypeId: ''
      };

      await LocalNotifications.schedule({
        notifications: [notification]
      });
    } catch (error) {
      console.error('Schedule notification error:', error);
      throw new Error('Failed to schedule notification');
    }
  }

  static async sendPushNotification(
    to: string,
    title: string,
    body: string
  ): Promise<void> {
    // This would typically be handled by your backend service
    // Here's an example of how to register for push notifications
    try {
      await PushNotifications.register();

      PushNotifications.addListener('registration', (token) => {
        console.log('Push registration success, token: ' + token.value);
        // Send this token to your backend
      });

      PushNotifications.addListener('registrationError', (error) => {
        console.error('Error on registration: ' + JSON.stringify(error.error));
      });

      PushNotifications.addListener(
        'pushNotificationReceived',
        (notification: PushNotificationSchema) => {
          console.log('Push notification received: ', notification);
        }
      );
    } catch (error) {
      console.error('Push notification error:', error);
    }
  }

  // File System Operations
  static async savePhotoToFile(photo: CameraPhoto): Promise<string> {
    try {
      const base64Data = await this.readAsBase64(photo);
      const fileName = new Date().getTime() + '.jpeg';
      const savedFile = await Filesystem.writeFile({
        path: fileName,
        data: base64Data,
        directory: Directory.Data
      });

      return savedFile.uri;
    } catch (error) {
      console.error('Save photo error:', error);
      throw new Error('Failed to save photo');
    }
  }

  static async readFileAsBase64(filePath: string): Promise<string> {
    try {
      const file = await Filesystem.readFile({
        path: filePath
      });
      return file.data as string;
    } catch (error) {
      console.error('Read file error:', error);
      throw new Error('Failed to read file');
    }
  }

  private static async readAsBase64(photo: CameraPhoto): Promise<string> {
    if (photo.webviewPath) {
      // Fetch the photo, read as a blob, then convert to base64 format
      const response = await fetch(photo.webviewPath);
      const blob = await response.blob();
      return await this.convertBlobToBase64(blob) as string;
    } else {
      // For Capacitor on Android/iOS, we can read directly from file system
      return await this.readFileAsBase64(photo.filepath);
    }
  }

  private static convertBlobToBase64(blob: Blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onerror = reject;
      reader.onload = () => {
        resolve(reader.result);
      };
      reader.readAsDataURL(blob);
    });
  }

  // Share Operations
  static async shareContent(
    title: string,
    text?: string,
    url?: string,
    dialogTitle?: string
  ): Promise<void> {
    try {
      await Share.share({
        title,
        text,
        url,
        dialogTitle: dialogTitle || 'Share with buddies'
      });
    } catch (error) {
      console.error('Share error:', error);
      throw new Error('Failed to share content');
    }
  }
}

// src/components/DeviceFeatures.tsx (React Component)
import React, { useState, useEffect } from 'react';
import { DeviceService, DevicePosition, CameraPhoto } from '../services/DeviceService';

const DeviceFeatures: React.FC = () => {
  const [position, setPosition] = useState<DevicePosition | null>(null);
  const [photo, setPhoto] = useState<CameraPhoto | null>(null);
  const [watchId, setWatchId] = useState<string | null>(null);

  useEffect(() => {
    requestPermissions();
    return () => {
      if (watchId) {
        Geolocation.clearWatch({ id: watchId });
      }
    };
  }, []);

  const requestPermissions = async () => {
    await DeviceService.requestNotificationPermission();
  };

  const handleTakePhoto = async () => {
    try {
      const newPhoto = await DeviceService.takePhoto();
      setPhoto(newPhoto);
      await DeviceService.savePhotoToFile(newPhoto);
    } catch (error) {
      console.error('Photo error:', error);
    }
  };

  const handleGetCurrentPosition = async () => {
    try {
      const currentPosition = await DeviceService.getCurrentPosition();
      setPosition(currentPosition);
    } catch (error) {
      console.error('Position error:', error);
    }
  };

  const handleWatchPosition = async () => {
    try {
      const id = await DeviceService.watchPosition((pos) => {
        setPosition(pos);
      });
      setWatchId(id);
    } catch (error) {
      console.error('Watch position error:', error);
    }
  };

  const handleScheduleNotification = async () => {
    try {
      await DeviceService.scheduleLocalNotification(
        'Hello from Capacitor!',
        'This is a local notification.',
        { at: new Date(Date.now() + 5000) } // 5 seconds from now
      );
    } catch (error) {
      console.error('Notification error:', error);
    }
  };

  const handleShare = async () => {
    try {
      await DeviceService.shareContent(
        'Check out my location!',
        `I'm at lat: ${position?.latitude}, lng: ${position?.longitude}`,
        undefined,
        'Share Location'
      );
    } catch (error) {
      console.error('Share error:', error);
    }
  };

  return (
    <div className="device-features">
      <h2>Capacitor Device APIs Demo</h2>

      <section>
        <h3>Camera</h3>
        <button onClick={handleTakePhoto}>Take Photo</button>
        {photo && (
          <div>
            <img src={photo.webviewPath} alt="Taken photo" style={{ maxWidth: '200px' }} />
          </div>
        )}
      </section>

      <section>
        <h3>Geolocation</h3>
        <button onClick={handleGetCurrentPosition}>Get Current Position</button>
        <button onClick={handleWatchPosition}>Watch Position</button>
        {position && (
          <div>
            <p>Latitude: {position.latitude}</p>
            <p>Longitude: {position.longitude}</p>
            <p>Accuracy: {position.accuracy}m</p>
            <p>Timestamp: {new Date(position.timestamp).toLocaleString()}</p>
            <button onClick={handleShare}>Share Location</button>
          </div>
        )}
      </section>

      <section>
        <h3>Notifications</h3>
        <button onClick={handleScheduleNotification}>
          Schedule Notification (5s)
        </button>
      </section>
    </div>
  );
};

export default DeviceFeatures;

💻 Разработка Кастомных Плагинов Capacitor typescript

🟡 intermediate

Создать кастомные плагины Capacitor для специфичной функциональности платформы с реализациями на Swift (iOS) и Java (Android)

// src/definitions.ts
import { PluginListenerHandle } from '@capacitor/core';

export interface MyCustomPlugin {
  /**
   * Perform a custom operation
   */
  customOperation(options: CustomOperationOptions): Promise<CustomOperationResult>;

  /**
   * Get device information
   */
  getDeviceInfo(): Promise<DeviceInfo>;

  /**
   * Listen for custom events
   */
  addListener(
    eventName: 'customEvent',
    listenerFunc: (event: CustomEvent) => void
  ): Promise<PluginListenerHandle> & PluginListenerHandle;

  /**
   * Remove all listeners for this plugin
   */
  removeAllListeners(): Promise<void>;
}

export interface CustomOperationOptions {
  value: string;
  mode?: 'normal' | 'advanced';
}

export interface CustomOperationResult {
  success: boolean;
  processedValue: string;
  timestamp: number;
}

export interface DeviceInfo {
  platform: string;
  version: string;
  customProperty: string;
}

export interface CustomEvent {
  data: any;
  timestamp: number;
}

// src/web.ts
import { WebPlugin } from '@capacitor/core';
import { MyCustomPlugin, CustomOperationOptions, CustomOperationResult, DeviceInfo, CustomEvent } from './definitions';

export class MyCustomPluginWeb extends WebPlugin implements MyCustomPlugin {
  constructor() {
    super();
  }

  async customOperation(options: CustomOperationOptions): Promise<CustomOperationResult> {
    // Web implementation
    const processedValue = options.mode === 'advanced'
      ? `ADVANCED: ${options.value}`
      : `NORMAL: ${options.value}`;

    return {
      success: true,
      processedValue,
      timestamp: Date.now()
    };
  }

  async getDeviceInfo(): Promise<DeviceInfo> {
    return {
      platform: 'web',
      version: navigator.userAgent,
      customProperty: 'web-specific-value'
    };
  }

  async addListener(eventName: 'customEvent', listenerFunc: (event: CustomEvent) => void): Promise<any> {
    // Web event listener implementation
    window.addEventListener(eventName, (event: any) => {
      listenerFunc(event.detail);
    });
  }

  async removeAllListeners(): Promise<void> {
    // Web cleanup
    window.removeEventListener('customEvent', () => {});
  }
}

// src/index.ts
import { registerPlugin } from '@capacitor/core';
import type { MyCustomPlugin } from './definitions';

const MyCustomPlugin = registerPlugin<MyCustomPlugin>('MyCustomPlugin', {
  web: () => import('./web').then(m => new m.MyCustomPluginWeb()),
});

export * from './definitions';
export { MyCustomPlugin };

// iOS Plugin (Swift) - ios/Plugin/MyCustomPlugin.swift
import Foundation
import Capacitor

@objc(MyCustomPlugin)
public class MyCustomPlugin: CAPPlugin, CAPPluginMethod {
    @objc func customOperation(_ call: CAPPluginCall) {
        guard let value = call.getString("value") else {
            call.reject("Missing required option: value")
            return
        }

        let mode = call.getString("mode") ?? "normal"
        let processedValue = mode == "advanced" ? "ADVANCED: \(value)" : "NORMAL: \(value)"

        let result = [
            "success": true,
            "processedValue": processedValue,
            "timestamp": Date().timeIntervalSince1970 * 1000
        ] as [String : Any]

        call.resolve(result)
    }

    @objc func getDeviceInfo(_ call: CAPPluginCall) {
        let device = UIDevice.current
        let result = [
            "platform": "ios",
            "version": device.systemVersion,
            "customProperty": "ios-specific-value"
        ]

        call.resolve(result)
    }

    @objc override public func load() {
        // Plugin initialization
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleCustomNotification),
            name: NSNotification.Name("CustomEvent"),
            object: nil
        )
    }

    @objc func handleCustomNotification(_ notification: Notification) {
        guard let data = notification.userInfo else { return }

        let eventData = [
            "data": data,
            "timestamp": Date().timeIntervalSince1970 * 1000
        ] as [String : Any]

        notifyListeners("customEvent", data: eventData)
    }
}

// iOS Plugin (Objective-C) - ios/Plugin/MyCustomPluginPlugin.m
#import <Foundation/Foundation.h>
#import <Capacitor/Capacitor.h>

// Define the plugin using CAP_PLUGIN Macro
CAP_PLUGIN(MyCustomPlugin, "MyCustomPlugin",
    CAP_PLUGIN_METHOD(customOperation, CAPPluginReturnPromise);
    CAP_PLUGIN_METHOD(getDeviceInfo, CAPPluginReturnPromise);
)

// Android Plugin (Java) - com.example.myapp/MyCustomPlugin.java
package com.example.myapp;

import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;

@CapacitorPlugin(name = "MyCustomPlugin")
public class MyCustomPlugin extends Plugin {

    @PluginMethod
    public void customOperation(PluginCall call) {
        String value = call.getString("value");
        if (value == null) {
            call.reject("Missing required option: value");
            return;
        }

        String mode = call.getString("mode", "normal");
        String processedValue = "advanced".equals(mode)
            ? "ADVANCED: " + value
            : "NORMAL: " + value;

        JSObject result = new JSObject();
        result.put("success", true);
        result.put("processedValue", processedValue);
        result.put("timestamp", System.currentTimeMillis());

        call.resolve(result);
    }

    @PluginMethod
    public void getDeviceInfo(PluginCall call) {
        JSObject result = new JSObject();
        result.put("platform", "android");
        result.put("version", android.os.Build.VERSION.RELEASE);
        result.put("customProperty", "android-specific-value");

        call.resolve(result);
    }

    @Override
    public void load() {
        // Plugin initialization
    }
}

// Usage in your app
import { MyCustomPlugin } from 'capacitor-my-custom-plugin';

class CustomPluginService {
  static async performCustomOperation(value: string, mode?: 'normal' | 'advanced') {
    try {
      const result = await MyCustomPlugin.customOperation({ value, mode });
      console.log('Custom operation result:', result);
      return result;
    } catch (error) {
      console.error('Custom operation error:', error);
      throw error;
    }
  }

  static async getDeviceInformation() {
    try {
      const deviceInfo = await MyCustomPlugin.getDeviceInfo();
      console.log('Device info:', deviceInfo);
      return deviceInfo;
    } catch (error) {
      console.error('Get device info error:', error);
      throw error;
    }
  }

  static async listenForCustomEvents() {
    await MyCustomPlugin.addListener('customEvent', (event) => {
      console.log('Custom event received:', event);
      // Handle the event
    });
  }
}

💻 Развертывание и Оптимизация PWA Capacitor typescript

🔴 complex

Настроить приложение Capacitor как Progressive Web App с оффлайн возможностями, service workers и оптимизацией манифеста приложения

// public/manifest.json
{
  "name": "Capacitor PWA Demo",
  "short_name": "CapacitorPWA",
  "description": "A progressive web app built with Capacitor",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#3880ff",
  "theme_color": "#3880ff",
  "orientation": "portrait",
  "icons": [
    {
      "src": "assets/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "assets/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "assets/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "assets/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "assets/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "assets/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "assets/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "assets/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "splash_pages": null
}

// public/sw.js (Service Worker)
const CACHE_NAME = 'capacitor-pwa-v1';
const STATIC_CACHE = 'static-cache-v1';
const DYNAMIC_CACHE = 'dynamic-cache-v1';

const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/assets/icons/icon-192x192.png',
  '/assets/icons/icon-512x512.png',
  '/manifest.json'
];

// Install event - cache static assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE)
      .then((cache) => {
        console.log('Service Worker: Caching static assets');
        return cache.addAll(STATIC_ASSETS);
      })
      .then(() => self.skipWaiting())
  );
});

// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys()
      .then((cacheNames) => {
        return Promise.all(
          cacheNames.map((cache) => {
            if (cache !== STATIC_CACHE && cache !== DYNAMIC_CACHE) {
              console.log('Service Worker: Clearing old cache');
              return caches.delete(cache);
            }
          })
        );
      })
      .then(() => self.clients.claim())
  );
});

// Fetch event - implement caching strategies
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        // Cache first strategy for static assets
        if (event.request.destination === 'script' ||
            event.request.destination === 'style' ||
            event.request.destination === 'image') {
          return response || fetch(event.request);
        }

        // Network first strategy for API calls
        if (event.request.url.includes('/api/')) {
          return fetch(event.request)
            .then((fetchResponse) => {
              // Cache successful API responses
              if (fetchResponse.status === 200) {
                caches.open(DYNAMIC_CACHE)
                  .then((cache) => cache.put(event.request, fetchResponse.clone()));
              }
              return fetchResponse;
            })
            .catch(() => {
              // Fallback to cached API response if network fails
              return caches.match(event.request);
            });
        }

        // Network first for HTML pages
        if (event.request.destination === 'document') {
          return fetch(event.request)
            .then((fetchResponse) => {
              // Cache successful page responses
              if (fetchResponse.status === 200) {
                caches.open(DYNAMIC_CACHE)
                  .then((cache) => cache.put(event.request, fetchResponse.clone()));
              }
              return fetchResponse;
            })
            .catch(() => {
              // Fallback to cached page or offline page
              return caches.match(event.request) ||
                     caches.match('/offline.html') ||
                     new Response('Offline', { status: 503 });
            });
        }

        return fetch(event.request);
      })
  );
});

// Background sync for offline actions
self.addEventListener('sync', (event) => {
  if (event.tag === 'background-sync') {
    event.waitUntil(doBackgroundSync());
  }
});

async function doBackgroundSync() {
  // Handle offline data synchronization
  const offlineActions = await getOfflineActions();

  for (const action of offlineActions) {
    try {
      await processOfflineAction(action);
      await removeOfflineAction(action.id);
    } catch (error) {
      console.error('Background sync failed:', error);
    }
  }
}

// Push notifications
self.addEventListener('push', (event) => {
  const options = {
    body: event.data.text(),
    icon: '/assets/icons/icon-192x192.png',
    badge: '/assets/icons/icon-72x72.png',
    vibrate: [100, 50, 100],
    data: {
      dateOfArrival: Date.now(),
      primaryKey: 1
    },
    actions: [
      {
        action: 'explore',
        title: 'Explore',
        icon: '/assets/icons/checkmark.png'
      },
      {
        action: 'close',
        title: 'Close',
        icon: '/assets/icons/xmark.png'
      }
    ]
  };

  event.waitUntil(
    self.registration.showNotification('Push Notification', options)
  );
});

// src/services/PWAService.ts
import { Capacitor } from '@capacitor/core';

export interface OfflineAction {
  id: string;
  type: string;
  data: any;
  timestamp: number;
}

export class PWAService {
  private static isPWA(): boolean {
    return Capacitor.getPlatform() === 'web' &&
           (window.matchMedia('(display-mode: standalone)').matches ||
            (window.navigator as any).standalone);
  }

  private static isInStandaloneMode(): boolean {
    return ('standalone' in window.navigator && (window.navigator as any).standalone) ||
           window.matchMedia('(display-mode: standalone)').matches;
  }

  static async installPWA(): Promise<void> {
    if (!this.isPWA() && 'serviceWorker' in navigator) {
      try {
        const registration = await navigator.serviceWorker.register('/sw.js');
        console.log('Service Worker registered:', registration);

        // Show install prompt
        this.showInstallPrompt();
      } catch (error) {
        console.error('Service Worker registration failed:', error);
      }
    }
  }

  private static deferredPrompt: any;

  static showInstallPrompt(): void {
    window.addEventListener('beforeinstallprompt', (e) => {
      e.preventDefault();
      this.deferredPrompt = e;

      // Show custom install UI
      this.showInstallUI();
    });
  }

  private static showInstallUI(): void {
    const installButton = document.createElement('button');
    installButton.textContent = 'Install App';
    installButton.style.cssText = `
      position: fixed;
      bottom: 20px;
      right: 20px;
      padding: 12px 24px;
      background: #3880ff;
      color: white;
      border: none;
      border-radius: 8px;
      cursor: pointer;
      z-index: 1000;
      font-size: 16px;
    `;

    installButton.addEventListener('click', async () => {
      if (this.deferredPrompt) {
        this.deferredPrompt.prompt();
        const { outcome } = await this.deferredPrompt.userChoice;

        if (outcome === 'accepted') {
          console.log('PWA installation accepted');
        } else {
          console.log('PWA installation dismissed');
        }

        this.deferredPrompt = null;
        installButton.remove();
      }
    });

    document.body.appendChild(installButton);
  }

  static async saveOfflineAction(action: Omit<OfflineAction, 'id' | 'timestamp'>): Promise<void> {
    const offlineAction: OfflineAction = {
      ...action,
      id: `${Date.now()}-${Math.random()}`,
      timestamp: Date.now()
    };

    const existingActions = await this.getOfflineActions();
    existingActions.push(offlineAction);

    localStorage.setItem('offlineActions', JSON.stringify(existingActions));

    // Register background sync if available
    if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
      const registration = await navigator.serviceWorker.ready;
      await registration.sync.register('background-sync');
    }
  }

  static async getOfflineActions(): Promise<OfflineAction[]> {
    const stored = localStorage.getItem('offlineActions');
    return stored ? JSON.parse(stored) : [];
  }

  static async removeOfflineAction(id: string): Promise<void> {
    const actions = await this.getOfflineActions();
    const filtered = actions.filter(action => action.id !== id);
    localStorage.setItem('offlineActions', JSON.stringify(filtered));
  }

  static isOnline(): boolean {
    return navigator.onLine;
  }

  static async checkConnectivity(): Promise<boolean> {
    if (!this.isOnline()) {
      return false;
    }

    try {
      const response = await fetch('/api/health', {
        method: 'HEAD',
        cache: 'no-cache'
      });
      return response.ok;
    } catch {
      return false;
    }
  }

  static setupConnectivityListeners(callback: (isOnline: boolean) => void): void {
    window.addEventListener('online', () => callback(true));
    window.addEventListener('offline', () => callback(false));
  }

  static async getAppVersion(): Promise<string> {
    try {
      const response = await fetch('/manifest.json');
      const manifest = await response.json();
      return manifest.version || '1.0.0';
    } catch {
      return '1.0.0';
    }
  }

  static getDeviceInfo(): { isPWA: boolean; isStandalone: boolean; platform: string } {
    return {
      isPWA: this.isPWA(),
      isStandalone: this.isInStandaloneMode(),
      platform: Capacitor.getPlatform()
    };
  }
}

// src/App.tsx (React Integration)
import React, { useState, useEffect } from 'react';
import { PWAService } from './services/PWAService';

const App: React.FC = () => {
  const [isOnline, setIsOnline] = useState(PWAService.isOnline());
  const [appInfo, setAppInfo] = useState(PWAService.getDeviceInfo());

  useEffect(() => {
    // Initialize PWA
    PWAService.installPWA();

    // Setup connectivity listeners
    PWAService.setupConnectivityListeners(setIsOnline);

    // Check app version
    PWAService.getAppVersion().then(version => {
      console.log('App version:', version);
    });

    // Handle offline actions when coming back online
    if (isOnline) {
      PWAService.getOfflineActions().then(actions => {
        if (actions.length > 0) {
          console.log('Processing offline actions:', actions.length);
        }
      });
    }
  }, [isOnline]);

  return (
    <div className="app">
      <header>
        <h1>Capacitor PWA Demo</h1>
        <div className="status">
          <span className={isOnline ? 'online' : 'offline'}>
            {isOnline ? '🟢 Online' : '🔴 Offline'}
          </span>
          <span>Platform: {appInfo.platform}</span>
          {appInfo.isPWA && <span>PWA Mode</span>}
        </div>
      </header>

      <main>
        {/* Your app content */}
      </main>
    </div>
  );
};

export default App;