Capacitor
45 minutes
linux
macos
windows

Native Features

Access device capabilities like camera, GPS, and push notifications using Capacitor plugins.

What you'll accomplish

Implement camera, geolocation, and storage features in your Capacitor app.

Prerequisites

  • Completed iOS and/or Android setup
  • Running Capacitor app

How Plugins Work

Capacitor plugins bridge JavaScript and native code:

Your JavaScript Code
        ↓
    Capacitor Plugin API
        ↓
    Native Implementation (Swift/Kotlin)
        ↓
    Device Hardware/OS Features

You write JavaScript - the plugin handles the native complexity.

Installing Plugins

All Capacitor plugins follow the same pattern:

bash
# 1. Install with npm
npm install @capacitor/plugin-name

# 2. Sync to native projects
npx cap sync

Then import and use in your code:

typescript
import { PluginName } from '@capacitor/plugin-name';

await PluginName.doSomething();

Camera

Take photos and access the photo library.

Installation

bash
npm install @capacitor/camera
npx cap sync

iOS Configuration

Add to ios/App/App/Info.plist:

xml
<key>NSCameraUsageDescription</key>
<string>We need camera access to take photos</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need photo library access to select images</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need to save photos to your library</string>

Android Configuration

Add to android/app/src/main/AndroidManifest.xml:

xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

Usage

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

// Take a photo
async function takePhoto() {
  const image = await Camera.getPhoto({
    quality: 90,
    allowEditing: true,
    resultType: CameraResultType.Uri,
    source: CameraSource.Camera,
  });

  // image.webPath is a URL you can use in <img src="">
  return image.webPath;
}

// Pick from gallery
async function pickFromGallery() {
  const image = await Camera.getPhoto({
    quality: 90,
    resultType: CameraResultType.Uri,
    source: CameraSource.Photos,
  });

  return image.webPath;
}

// Get as base64
async function getPhotoBase64() {
  const image = await Camera.getPhoto({
    quality: 90,
    resultType: CameraResultType.Base64,
    source: CameraSource.Prompt,  // Let user choose camera or gallery
  });

  return `data:image/jpeg;base64,${image.base64String}`;
}

React Component Example

tsx
'use client';

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

export function PhotoCapture() {
  const [photo, setPhoto] = useState<string | null>(null);

  async function handleTakePhoto() {
    try {
      const image = await Camera.getPhoto({
        quality: 90,
        allowEditing: true,
        resultType: CameraResultType.Uri,
      });
      setPhoto(image.webPath || null);
    } catch (error) {
      console.error('Camera error:', error);
    }
  }

  return (
    <div>
      <button onClick={handleTakePhoto}>Take Photo</button>
      {photo && <img src={photo} alt="Captured" />}
    </div>
  );
}

Geolocation

Get the user's location.

Installation

bash
npm install @capacitor/geolocation
npx cap sync

iOS Configuration

Add to ios/App/App/Info.plist:

xml
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show nearby places</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>We need background location for navigation</string>

Android Configuration

Add to android/app/src/main/AndroidManifest.xml:

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

Usage

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

// Get current position
async function getCurrentLocation() {
  const position = await Geolocation.getCurrentPosition();

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

// Watch position changes
function watchLocation(callback: (lat: number, lng: number) => void) {
  const watchId = Geolocation.watchPosition(
    { enableHighAccuracy: true },
    (position, err) => {
      if (position) {
        callback(position.coords.latitude, position.coords.longitude);
      }
    }
  );

  // Return cleanup function
  return () => Geolocation.clearWatch({ id: watchId });
}

// Check/request permissions
async function checkPermissions() {
  const status = await Geolocation.checkPermissions();

  if (status.location !== 'granted') {
    const request = await Geolocation.requestPermissions();
    return request.location === 'granted';
  }

  return true;
}

React Hook Example

tsx
'use client';

import { useState, useEffect } from 'react';
import { Geolocation } from '@capacitor/geolocation';

export function useLocation() {
  const [location, setLocation] = useState<{ lat: number; lng: number } | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let watchId: string;

    async function startWatching() {
      try {
        await Geolocation.requestPermissions();

        watchId = await Geolocation.watchPosition(
          { enableHighAccuracy: true },
          (position, err) => {
            if (err) {
              setError(err.message);
              return;
            }
            if (position) {
              setLocation({
                lat: position.coords.latitude,
                lng: position.coords.longitude,
              });
            }
          }
        );
      } catch (e) {
        setError('Failed to get location');
      }
    }

    startWatching();

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

  return { location, error };
}

Local Storage

Persist data locally with the Preferences plugin (key-value storage).

Installation

bash
npm install @capacitor/preferences
npx cap sync

Usage

typescript
import { Preferences } from '@capacitor/preferences';

// Save data
async function saveUser(user: { id: string; name: string }) {
  await Preferences.set({
    key: 'user',
    value: JSON.stringify(user),
  });
}

// Load data
async function loadUser() {
  const { value } = await Preferences.get({ key: 'user' });
  return value ? JSON.parse(value) : null;
}

// Remove data
async function clearUser() {
  await Preferences.remove({ key: 'user' });
}

// Clear all data
async function clearAll() {
  await Preferences.clear();
}

// Get all keys
async function getAllKeys() {
  const { keys } = await Preferences.keys();
  return keys;
}

React Hook Example

tsx
'use client';

import { useState, useEffect, useCallback } from 'react';
import { Preferences } from '@capacitor/preferences';

export function useStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(initialValue);
  const [loading, setLoading] = useState(true);

  // Load on mount
  useEffect(() => {
    async function load() {
      const { value: stored } = await Preferences.get({ key });
      if (stored) {
        setValue(JSON.parse(stored));
      }
      setLoading(false);
    }
    load();
  }, [key]);

  // Save function
  const save = useCallback(async (newValue: T) => {
    setValue(newValue);
    await Preferences.set({
      key,
      value: JSON.stringify(newValue),
    });
  }, [key]);

  return { value, save, loading };
}

Push Notifications

Receive push notifications.

Installation

bash
npm install @capacitor/push-notifications
npx cap sync

iOS Configuration

Push notifications require:

  1. Apple Developer account
  2. Push notification capability in Xcode
  3. APNs configuration

Android Configuration

Requires Firebase Cloud Messaging setup:

  1. Create Firebase project
  2. Add google-services.json to android/app/
  3. Configure Gradle files

Usage

typescript
import { PushNotifications } from '@capacitor/push-notifications';

// Request permission and register
async function initPushNotifications() {
  // Request permission
  const permission = await PushNotifications.requestPermissions();

  if (permission.receive !== 'granted') {
    console.log('Push notification permission denied');
    return;
  }

  // Register with APNs/FCM
  await PushNotifications.register();

  // Listen for registration
  PushNotifications.addListener('registration', (token) => {
    console.log('Push registration token:', token.value);
    // Send token to your server
  });

  // Listen for errors
  PushNotifications.addListener('registrationError', (error) => {
    console.error('Push registration error:', error);
  });

  // Listen for notifications received
  PushNotifications.addListener('pushNotificationReceived', (notification) => {
    console.log('Push received:', notification);
  });

  // Listen for notification taps
  PushNotifications.addListener('pushNotificationActionPerformed', (notification) => {
    console.log('Push tapped:', notification);
    // Navigate to relevant screen
  });
}
Note

Push notifications require additional backend setup. You'll need a server to send notifications through APNs (iOS) or FCM (Android).

Your Mobile App Needs a Backend

Native features like camera, GPS, and local storage happen on the device. But most apps also need cloud services:

  • User accounts and authentication - login, sessions, password reset
  • Cloud data storage and sync - persist data across devices
  • Push notifications from your server - not just local notifications
  • In-app purchases and subscriptions - monetization

Backend Options for Capacitor Apps

ApproachBest forTime to implement
FirebasePrototypes, simple appsHours
SupabaseOpen-source preference, PostgreSQLDays
StackForDevsSaaS apps with billing/multi-tenancyHours
Custom backend (AWS/GCP/etc.)Full control, specific requirementsWeeks

For apps that need subscription billing and multi-tenancy (like a B2B mobile app), StackForDevs provides these as APIs you call from your Capacitor app—the same way you'd call any REST API from your frontend code.

Your Capacitor app is just a web app with native wrappers. Call APIs the same way you would in any web app:

typescript
// Example: Authenticate user via backend API
async function login(email: string, password: string) {
  const response = await fetch('https://your-backend.com/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });

  const { token, user } = await response.json();

  // Store token locally using Capacitor Preferences
  await Preferences.set({ key: 'auth_token', value: token });

  return user;
}

The backend handles the complexity (auth, billing, notifications), and your mobile app focuses on the user experience.

Share

Use the native share dialog.

Installation

bash
npm install @capacitor/share
npx cap sync

Usage

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

// Share text
async function shareText() {
  await Share.share({
    title: 'Check this out!',
    text: 'I found this amazing app',
    url: 'https://example.com',
    dialogTitle: 'Share with friends',
  });
}

// Check if sharing is available
async function canShare() {
  const { value } = await Share.canShare();
  return value;
}

Haptics

Provide tactile feedback.

Installation

bash
npm install @capacitor/haptics
npx cap sync

Usage

typescript
import { Haptics, ImpactStyle } from '@capacitor/haptics';

// Light tap feedback
async function lightTap() {
  await Haptics.impact({ style: ImpactStyle.Light });
}

// Medium impact
async function mediumImpact() {
  await Haptics.impact({ style: ImpactStyle.Medium });
}

// Heavy impact
async function heavyImpact() {
  await Haptics.impact({ style: ImpactStyle.Heavy });
}

// Vibration
async function vibrate() {
  await Haptics.vibrate({ duration: 300 });
}

// Notification feedback
async function notificationFeedback() {
  await Haptics.notification({ type: 'SUCCESS' });
}

Platform Detection

Run different code on different platforms.

typescript
import { Capacitor } from '@capacitor/core';

// Check platform
const platform = Capacitor.getPlatform();
// Returns: 'ios', 'android', or 'web'

// Check if running natively
const isNative = Capacitor.isNativePlatform();

// Platform-specific code
if (Capacitor.getPlatform() === 'ios') {
  // iOS-specific logic
} else if (Capacitor.getPlatform() === 'android') {
  // Android-specific logic
} else {
  // Web fallback
}

// Check if plugin is available
const hasCamera = Capacitor.isPluginAvailable('Camera');

Common Issues

Frequently Asked Questions

Make sure you:

  1. Ran npm install @capacitor/plugin-name
  2. Ran npx cap sync
  3. Rebuilt the native app

For iOS, also run:

bash
cd ios && pod install && cd ..

Check that you added the required Info.plist (iOS) or AndroidManifest.xml (Android) entries. Each plugin's documentation specifies required permissions.

Some features have platform differences. Check the plugin documentation for platform-specific notes. Use Capacitor.isPluginAvailable() to check.

Some plugins have web implementations, others don't. Check if the plugin supports web in its documentation. You may need to implement web fallbacks.

  1. You need a real device (not simulator/emulator for full testing)
  2. Configure APNs (iOS) or FCM (Android)
  3. Use tools like Firebase Console to send test notifications

Verify Your Understanding

You've completed this lesson if you can:

0/5

Next: Deploy your app to the Apple App Store and Google Play Store.