ScaleRocket/Mobile

Image Picker

Using expo-image-picker to select images from the gallery, compress them, and upload to your backend.

Image Picker

expo-image-picker lets users select photos from their device gallery or take new photos. ScaleRocket Mobile uses it for avatar uploads, post images, and document attachments.

Setup

npx expo install expo-image-picker

Add permission descriptions to app.json:

{
  "expo": {
    "ios": {
      "infoPlist": {
        "NSPhotoLibraryUsageDescription": "We access your photos to let you upload a profile picture"
      }
    }
  }
}

Pick an Image

import * as ImagePicker from "expo-image-picker";

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

  if (!result.canceled) {
    const image = result.assets[0];
    console.log("Selected:", image.uri);
    return image;
  }
}
OptionValuePurpose
mediaTypesImages, Videos, AllFilter media type
allowsEditingtrueShow crop/resize UI
aspect[1, 1]Crop aspect ratio (iOS only)
quality0.0 - 1.0Image compression

Take a Photo

Use launchCameraAsync to open the camera directly:

async function takePhoto() {
  const permission = await ImagePicker.requestCameraPermissionsAsync();
  if (!permission.granted) {
    Alert.alert("Permission required", "Camera access is needed to take photos");
    return;
  }

  const result = await ImagePicker.launchCameraAsync({
    allowsEditing: true,
    aspect: [1, 1],
    quality: 0.8,
  });

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

Image Compression

Compress images before uploading to reduce bandwidth and storage:

import { manipulateAsync, SaveFormat } from "expo-image-manipulator";

async function compressImage(uri: string) {
  const result = await manipulateAsync(
    uri,
    [{ resize: { width: 800 } }],
    { compress: 0.7, format: SaveFormat.JPEG }
  );

  return result.uri;
}
npx expo install expo-image-manipulator

Upload to Backend

Upload to Supabase Storage:

import { supabase } from "@/lib/supabase";

async function uploadImage(uri: string, userId: string) {
  const compressedUri = await compressImage(uri);
  const fileName = `${userId}/avatar-${Date.now()}.jpg`;

  const formData = new FormData();
  formData.append("file", {
    uri: compressedUri,
    name: "avatar.jpg",
    type: "image/jpeg",
  } as any);

  const { data, error } = await supabase.storage
    .from("avatars")
    .upload(fileName, formData, { upsert: true });

  if (error) throw error;

  const { data: urlData } = supabase.storage
    .from("avatars")
    .getPublicUrl(fileName);

  return urlData.publicUrl;
}

Upload to Convex File Storage:

import { useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";

async function uploadImage(uri: string) {
  const compressedUri = await compressImage(uri);

  // Get upload URL
  const uploadUrl = await generateUploadUrl();

  // Upload the file
  const response = await fetch(compressedUri);
  const blob = await response.blob();

  const result = await fetch(uploadUrl, {
    method: "POST",
    body: blob,
    headers: { "Content-Type": "image/jpeg" },
  });

  const { storageId } = await result.json();
  return storageId;
}

Complete Avatar Upload Component

function AvatarUpload({ user }: { user: User }) {
  const [uploading, setUploading] = useState(false);
  const [avatarUri, setAvatarUri] = useState(user.avatarUrl);

  const handleUpload = async () => {
    const image = await pickImage();
    if (!image) return;

    setUploading(true);
    try {
      const url = await uploadImage(image.uri, user.id);
      setAvatarUri(url);
    } catch (error) {
      Alert.alert("Upload Failed", "Could not upload image. Try again.");
    } finally {
      setUploading(false);
    }
  };

  return (
    <TouchableOpacity onPress={handleUpload} disabled={uploading}>
      <Avatar source={avatarUri ? { uri: avatarUri } : undefined} name={user.name} size="lg" />
      {uploading && <ActivityIndicator style={styles.overlay} />}
    </TouchableOpacity>
  );
}

Multiple Image Selection

Select multiple images at once:

const result = await ImagePicker.launchImageLibraryAsync({
  mediaTypes: ImagePicker.MediaTypeOptions.Images,
  allowsMultipleSelection: true,
  selectionLimit: 5,
  quality: 0.8,
});

if (!result.canceled) {
  const images = result.assets; // Array of selected images
}

Notes

  • allowsEditing only supports single image selection
  • On iOS, aspect ratio is only enforced when allowsEditing is true
  • Large images should always be compressed before upload
  • Image picker works in Expo Go (unlike camera)

On this page