Skip to content

Fix: Capacitor Not Working — Build Failing, Plugins Not Loading, or Native Features Not Available

FixDevs ·

Quick Answer

How to fix Capacitor issues — project setup with Ionic or standalone, native plugin access, iOS and Android build errors, live reload, deep links, push notifications, and migration from Cordova.

The Problem

A Capacitor plugin call returns an error:

import { Camera, CameraResultType } from '@capacitor/camera';

const photo = await Camera.getPhoto({
  resultType: CameraResultType.Base64,
});
// Error: "Camera" plugin is not implemented on web

Or the native project doesn’t build:

npx cap sync
npx cap open ios
error: Unable to load contents of file list: '.../Build/Pods-App.xcfilelist'

Or cap sync fails with mismatched versions:

[error] @capacitor/[email protected] and @capacitor/[email protected] are incompatible.
Upgrade to matching versions.

Or live reload doesn’t connect to the device:

[error] Unable to connect to server at http://localhost:3000

Why This Happens

Capacitor wraps a web app inside a native WebView and provides a bridge to native APIs. The integration has several failure points:

  • Plugins have three implementations: web, iOS, Android — when running in the browser, only the web implementation is available. The Camera, Filesystem, and other device-specific plugins only work on actual devices or simulators. Calling them on the web throws “not implemented” unless the plugin has a web fallback.
  • Native projects must be in sync with the web projectcap sync copies the web build output into the native projects and updates native dependencies. Skipping this after adding a plugin or changing the config results in mismatches.
  • Version alignment is required@capacitor/core, @capacitor/ios, @capacitor/android, and all @capacitor/* plugins must be on the same major version. Mixing v4 and v5 packages causes runtime errors.
  • Live reload requires the device to reach the dev server — the device needs network access to the machine running the dev server. localhost doesn’t work from a physical device — use the machine’s local IP. Firewalls and network isolation often block the connection.

Fix 1: Set Up a Capacitor Project

# Add Capacitor to an existing web project
npm install @capacitor/core @capacitor/cli
npx cap init

# Add platforms
npm install @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android
// capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.myapp',
  appName: 'My App',
  webDir: 'dist',  // Where your web build output goes (dist, build, out)

  server: {
    // For live reload during development
    // url: 'http://192.168.1.100:3000',  // Your machine's local IP
    // cleartext: true,  // Allow HTTP (not HTTPS)
  },

  plugins: {
    SplashScreen: {
      launchAutoHide: true,
      launchShowDuration: 2000,
      backgroundColor: '#ffffff',
    },
    StatusBar: {
      style: 'dark',
    },
    Keyboard: {
      resize: 'body',
      resizeOnFullScreen: true,
    },
  },

  ios: {
    contentInset: 'automatic',
    scheme: 'My App',
  },

  android: {
    buildOptions: {
      keystorePath: undefined,
      keystoreAlias: undefined,
    },
  },
};

export default config;

Build and sync workflow:

# 1. Build your web app
npm run build

# 2. Copy web assets + sync native plugins
npx cap sync

# 3. Open in IDE
npx cap open ios      # Opens Xcode
npx cap open android  # Opens Android Studio

# Or run directly (Capacitor 5+)
npx cap run ios --target="iPhone 15"
npx cap run android

Fix 2: Use Plugins Correctly

Install each plugin and sync:

npm install @capacitor/camera @capacitor/filesystem @capacitor/geolocation @capacitor/haptics @capacitor/share @capacitor/local-notifications
npx cap sync
// Check platform before calling native-only plugins
import { Capacitor } from '@capacitor/core';

// Platform detection
const isNative = Capacitor.isNativePlatform();  // true on iOS/Android
const platform = Capacitor.getPlatform();         // 'ios', 'android', 'web'

// Camera — with web fallback
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';

async function takePhoto() {
  // Check if the plugin is available
  if (!Capacitor.isPluginAvailable('Camera')) {
    // Fallback: use file input on web
    return useFileInput();
  }

  // Request permissions first on native
  const permissions = await Camera.requestPermissions();
  if (permissions.camera !== 'granted') {
    throw new Error('Camera permission denied');
  }

  const photo = await Camera.getPhoto({
    resultType: CameraResultType.Uri,
    source: CameraSource.Camera,
    quality: 90,
    width: 1024,
    allowEditing: false,
  });

  return photo.webPath;  // Usable in <img src="">
}

// Filesystem — read/write app storage
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';

async function saveFile(filename: string, data: string) {
  await Filesystem.writeFile({
    path: filename,
    data,
    directory: Directory.Documents,
    encoding: Encoding.UTF8,
  });
}

async function readFile(filename: string) {
  const result = await Filesystem.readFile({
    path: filename,
    directory: Directory.Documents,
    encoding: Encoding.UTF8,
  });
  return result.data;
}

// Geolocation
import { Geolocation } from '@capacitor/geolocation';

async function getCurrentPosition() {
  const permissions = await Geolocation.requestPermissions();
  if (permissions.location !== 'granted') {
    throw new Error('Location permission denied');
  }

  const position = await Geolocation.getCurrentPosition({
    enableHighAccuracy: true,
    timeout: 10000,
  });

  return {
    lat: position.coords.latitude,
    lng: position.coords.longitude,
  };
}

// Share
import { Share } from '@capacitor/share';

async function shareContent() {
  await Share.share({
    title: 'Check this out',
    text: 'Sharing from my app',
    url: 'https://example.com',
    dialogTitle: 'Share with friends',
  });
}

Fix 3: Fix iOS Build Errors

Common Xcode build failures and their fixes:

# 1. Pod install failed — missing or outdated CocoaPods
cd ios/App && pod install --repo-update
# If that fails:
sudo gem install cocoapods
pod repo update
pod install

# 2. Clean build folder when Xcode caches cause issues
# In Xcode: Product → Clean Build Folder (Cmd+Shift+K)
# Or from terminal:
cd ios && xcodebuild clean

# 3. Version mismatch — update all Capacitor packages
npm install @capacitor/core@latest @capacitor/ios@latest @capacitor/cli@latest
npx cap sync ios

# 4. Minimum iOS version — update in Xcode or Podfile
# ios/App/Podfile
platform :ios, '14.0'  # Capacitor 5 requires iOS 14+

# 5. Signing issues — update Team in Xcode
# Xcode → App target → Signing & Capabilities → Team

iOS permissions — add to Info.plist:

<!-- ios/App/App/Info.plist -->
<!-- Camera -->
<key>NSCameraUsageDescription</key>
<string>We need camera access to take photos</string>

<!-- Photo Library -->
<key>NSPhotoLibraryUsageDescription</key>
<string>We need photo library access to select images</string>

<!-- Location -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show nearby places</string>

<!-- Microphone (for video recording) -->
<key>NSMicrophoneUsageDescription</key>
<string>We need microphone access for video recording</string>

Fix 4: Fix Android Build Errors

# 1. Gradle sync failed — update Gradle wrapper
cd android && ./gradlew wrapper --gradle-version=8.4

# 2. SDK version mismatch
# android/variables.gradle
ext {
    minSdkVersion = 22          // Capacitor 5 minimum
    compileSdkVersion = 34
    targetSdkVersion = 34
    androidxActivityVersion = '1.8.0'
    androidxAppCompatVersion = '1.6.1'
    androidxCoordinatorLayoutVersion = '1.2.0'
    androidxCoreVersion = '1.12.0'
    androidxFragmentVersion = '1.6.2'
    coreSplashScreenVersion = '1.0.1'
    androidxWebkitVersion = '1.9.0'
    junitVersion = '4.13.2'
    androidxJunitVersion = '1.1.5'
    androidxEspressoCoreVersion = '3.5.1'
}

# 3. Clean and rebuild
cd android && ./gradlew clean
npx cap sync android

Android permissions — add to AndroidManifest.xml:

<!-- android/app/src/main/AndroidManifest.xml -->
<manifest>
    <!-- Internet (usually already there) -->
    <uses-permission android:name="android.permission.INTERNET" />

    <!-- Camera -->
    <uses-permission android:name="android.permission.CAMERA" />

    <!-- Location -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <!-- Storage (Android 12 and below) -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application ...>
</manifest>

Fix 5: Live Reload on Device

// capacitor.config.ts — development config
const config: CapacitorConfig = {
  appId: 'com.example.myapp',
  appName: 'My App',
  webDir: 'dist',
  server: {
    // Use your machine's local IP — NOT localhost
    url: 'http://192.168.1.100:5173',
    cleartext: true,  // Allow HTTP on Android
  },
};
# Find your machine's local IP
# macOS:
ipconfig getifaddr en0

# Windows:
ipconfig | findstr "IPv4"

# Linux:
hostname -I

Automated live reload setup:

# Run dev server accessible on network
npm run dev -- --host 0.0.0.0

# Then sync and run on device
npx cap sync
npx cap run ios --livereload --external
# --external automatically sets the server URL to your IP

Android cleartext HTTP — required for live reload:

<!-- android/app/src/main/AndroidManifest.xml -->
<application
    android:usesCleartextTraffic="true"
    ...>

Fix 6: Push Notifications

npm install @capacitor/push-notifications
npx cap sync
import { PushNotifications } from '@capacitor/push-notifications';
import { Capacitor } from '@capacitor/core';

async function initPushNotifications() {
  if (!Capacitor.isNativePlatform()) return;

  // Request permission
  const permission = await PushNotifications.requestPermissions();
  if (permission.receive !== 'granted') {
    console.warn('Push notification permission denied');
    return;
  }

  // Register for push notifications
  await PushNotifications.register();

  // Get the device token
  PushNotifications.addListener('registration', (token) => {
    console.log('Push token:', token.value);
    // Send token to your backend
    fetch('/api/devices', {
      method: 'POST',
      body: JSON.stringify({ token: token.value, platform: Capacitor.getPlatform() }),
    });
  });

  // Registration error
  PushNotifications.addListener('registrationError', (error) => {
    console.error('Push registration failed:', error);
  });

  // Notification received while app is in foreground
  PushNotifications.addListener('pushNotificationReceived', (notification) => {
    console.log('Foreground notification:', notification);
    // Show in-app notification UI
  });

  // User tapped a notification
  PushNotifications.addListener('pushNotificationActionPerformed', (action) => {
    console.log('Notification tapped:', action.notification);
    // Navigate to relevant screen
    const data = action.notification.data;
    if (data.screen === 'order') {
      router.push(`/orders/${data.orderId}`);
    }
  });
}

iOS — enable Push Notifications capability in Xcode and upload your APNs key to Firebase or your push service.

Android — add google-services.json to android/app/ from the Firebase console.

Still Not Working?

“Plugin not implemented on web” — this is expected. Most hardware plugins (Camera, Geolocation, Haptics) have no web implementation. Use Capacitor.isNativePlatform() to conditionally call native APIs and provide web fallbacks. Some plugins like @capacitor/preferences (formerly Storage) work on all platforms.

cap sync runs but the plugin doesn’t appear in the native project — the plugin’s native code must be registered. For iOS, run cd ios/App && pod install. For Android, the plugin should auto-register via Gradle. If it doesn’t, check that the plugin package is in package.json dependencies (not devDependencies).

App works in browser but white screen on device — the web build output isn’t being served. Check that webDir in capacitor.config.ts matches your build output directory. Run npm run build before npx cap sync. Also check the browser console in Safari (iOS) or Chrome DevTools (Android) for JavaScript errors.

Deep links don’t open the app — deep linking requires native configuration beyond Capacitor. For iOS, add Associated Domains in Xcode and host an apple-app-site-association file. For Android, add intent filters in AndroidManifest.xml and host an assetlinks.json file. Use @capacitor/app plugin to handle the incoming URL.

For related mobile issues, see Fix: Expo Not Working and Fix: React Native Reanimated Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles