home / skills / thebushidocollective / han / expo-modules

This skill helps you integrate Expo SDK modules for camera, location, notifications, and files, handling permissions, configuration, and best practices.

npx playbooks add skill thebushidocollective/han --skill expo-modules

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.8 KB
---
name: expo-modules
user-invocable: false
description: Use when working with Expo SDK modules for camera, location, notifications, file system, and other device APIs. Covers permissions, configurations, and best practices.
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash
  - Grep
  - Glob
---

# Expo Modules

Use this skill when working with Expo's extensive SDK modules for accessing device features and native functionality.

## Key Concepts

### Camera

```tsx
import { Camera, CameraType } from 'expo-camera';
import { useState } from 'react';
import { Button, View } from 'react-native';

export default function CameraScreen() {
  const [permission, requestPermission] = Camera.useCameraPermissions();
  const [type, setType] = useState(CameraType.back);

  if (!permission?.granted) {
    return (
      <View>
        <Button title="Grant Permission" onPress={requestPermission} />
      </View>
    );
  }

  return (
    <Camera style={{ flex: 1 }} type={type}>
      <Button
        title="Flip Camera"
        onPress={() =>
          setType(type === CameraType.back ? CameraType.front : CameraType.back)
        }
      />
    </Camera>
  );
}
```

### Location

```tsx
import * as Location from 'expo-location';
import { useEffect, useState } from 'react';

export function useLocation() {
  const [location, setLocation] = useState<Location.LocationObject | null>(null);

  useEffect(() => {
    (async () => {
      const { status } = await Location.requestForegroundPermissionsAsync();
      if (status !== 'granted') return;

      const loc = await Location.getCurrentPositionAsync({});
      setLocation(loc);
    })();
  }, []);

  return location;
}
```

### Notifications

```tsx
import * as Notifications from 'expo-notifications';
import { useEffect } from 'react';

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
});

export function useNotifications() {
  useEffect(() => {
    const subscription = Notifications.addNotificationReceivedListener(
      (notification) => {
        console.log(notification);
      }
    );

    return () => subscription.remove();
  }, []);

  const sendNotification = async () => {
    await Notifications.scheduleNotificationAsync({
      content: {
        title: 'Hello!',
        body: 'This is a notification',
      },
      trigger: { seconds: 2 },
    });
  };

  return { sendNotification };
}
```

### File System

```tsx
import * as FileSystem from 'expo-file-system';

export async function saveFile(data: string, filename: string) {
  const uri = `${FileSystem.documentDirectory}${filename}`;
  await FileSystem.writeAsStringAsync(uri, data);
  return uri;
}

export async function readFile(filename: string) {
  const uri = `${FileSystem.documentDirectory}${filename}`;
  const content = await FileSystem.readAsStringAsync(uri);
  return content;
}

export async function downloadFile(url: string, filename: string) {
  const uri = `${FileSystem.documentDirectory}${filename}`;
  const download = await FileSystem.downloadAsync(url, uri);
  return download.uri;
}
```

### Image Picker

```tsx
import * as ImagePicker from 'expo-image-picker';
import { useState } from 'react';
import { Button, Image, View } from 'react-native';

export default function ImagePickerScreen() {
  const [image, setImage] = useState<string | null>(null);

  const pickImage = async () => {
    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images,
      allowsEditing: true,
      aspect: [4, 3],
      quality: 1,
    });

    if (!result.canceled) {
      setImage(result.assets[0].uri);
    }
  };

  return (
    <View>
      <Button title="Pick Image" onPress={pickImage} />
      {image && <Image source={{ uri: image }} style={{ width: 200, height: 200 }} />}
    </View>
  );
}
```

## Best Practices

### Permission Handling

```tsx
import * as Location from 'expo-location';

async function requestLocationPermission() {
  const { status: foregroundStatus } =
    await Location.requestForegroundPermissionsAsync();

  if (foregroundStatus !== 'granted') {
    console.log('Permission denied');
    return false;
  }

  // Request background permission only if needed
  const { status: backgroundStatus } =
    await Location.requestBackgroundPermissionsAsync();

  return backgroundStatus === 'granted';
}
```

### Secure Storage

```tsx
import * as SecureStore from 'expo-secure-store';

export async function saveToken(key: string, value: string) {
  await SecureStore.setItemAsync(key, value);
}

export async function getToken(key: string) {
  return await SecureStore.getItemAsync(key);
}

export async function deleteToken(key: string) {
  await SecureStore.deleteItemAsync(key);
}
```

### Device Info

```tsx
import * as Device from 'expo-device';
import * as Application from 'expo-application';
import Constants from 'expo-constants';

export function getDeviceInfo() {
  return {
    deviceName: Device.deviceName,
    deviceType: Device.deviceType,
    osName: Device.osName,
    osVersion: Device.osVersion,
    appVersion: Application.nativeApplicationVersion,
    buildVersion: Application.nativeBuildVersion,
    expoVersion: Constants.expoVersion,
  };
}
```

## Common Patterns

### Persistent Storage

```tsx
import AsyncStorage from '@react-native-async-storage/async-storage';

export const storage = {
  async set(key: string, value: any) {
    await AsyncStorage.setItem(key, JSON.stringify(value));
  },

  async get<T>(key: string): Promise<T | null> {
    const item = await AsyncStorage.getItem(key);
    return item ? JSON.parse(item) : null;
  },

  async remove(key: string) {
    await AsyncStorage.removeItem(key);
  },

  async clear() {
    await AsyncStorage.clear();
  },
};
```

### Background Tasks

```tsx
import * as BackgroundFetch from 'expo-background-fetch';
import * as TaskManager from 'expo-task-manager';

const BACKGROUND_FETCH_TASK = 'background-fetch';

TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
  // Do work here
  console.log('Background task running');
  return BackgroundFetch.BackgroundFetchResult.NewData;
});

export async function registerBackgroundTask() {
  return BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, {
    minimumInterval: 60 * 15, // 15 minutes
    stopOnTerminate: false,
    startOnBoot: true,
  });
}
```

### Sharing Content

```tsx
import * as Sharing from 'expo-sharing';

export async function shareContent(uri: string) {
  const isAvailable = await Sharing.isAvailableAsync();

  if (!isAvailable) {
    console.log('Sharing is not available');
    return;
  }

  await Sharing.shareAsync(uri, {
    mimeType: 'image/jpeg',
    dialogTitle: 'Share this image',
  });
}
```

## Related Skills

- **expo-config**: Configuring module permissions
- **expo-build**: Including modules in builds

Overview

This skill helps developers integrate Expo SDK modules for camera, location, notifications, file system, image picking, secure storage, device info, background tasks, and sharing. It focuses on practical examples, permission flows, and common patterns to access native device capabilities from TypeScript React Native apps. Use it to speed implementation and follow tested patterns for reliability and security.

How this skill works

It provides concise code patterns and helper functions showing how to request permissions, read/write files, pick images, schedule notifications, and run background work using Expo modules. The examples illustrate lifecycle hooks, permission checks, and simple wrappers for AsyncStorage, SecureStore, and FileSystem. You can copy the patterns into screens, hooks, or utilities and adapt configuration for your app and build targets.

When to use it

  • Access camera or image library in a screen or component
  • Request and manage foreground/background location permissions
  • Schedule and handle local notifications
  • Save, read, or download files to the app document directory
  • Run periodic background tasks or fetch data while the app is backgrounded

Best practices

  • Always request and check permissions before accessing hardware APIs; request background permissions only when needed
  • Store sensitive tokens in SecureStore instead of AsyncStorage
  • Use FileSystem.documentDirectory for user files and avoid storing large binaries in memory
  • Define Notification handlers and remove listeners on unmount to prevent leaks
  • Register background tasks with conservative intervals and test across device vendors

Example use cases

  • Camera screen with permission gating and front/back toggle
  • Hook that returns current location after requesting foreground permission
  • Utility to schedule a local notification and a listener for received notifications
  • Functions to save/read/download files using expo-file-system
  • Background fetch task that syncs data to the server every 15 minutes

FAQ

Do I need to ask for background permissions for location by default?

No. Request foreground location permission first and only request background permission if your feature requires continual tracking or background updates.

Where should I store API tokens on the device?

Use expo-secure-store for sensitive tokens; AsyncStorage is fine for non-sensitive cached data.