Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,14 @@
}
],
"expo-font",
"expo-web-browser"
"expo-web-browser",
[
"expo-image-picker",
{
"photosPermission": "Allow $(PRODUCT_NAME) to access your photos to upload receipt images for transaction extraction.",
"cameraPermission": "Allow $(PRODUCT_NAME) to access your camera to take photos of receipts for transaction extraction."
}
]
],
"experiments": {
"typedRoutes": true
Expand Down
38 changes: 7 additions & 31 deletions components/receipt-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import {
Image,
ScrollView,
ActivityIndicator,
Alert,
} from "react-native";
import { useState } from "react";
import * as ImagePicker from "expo-image-picker";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useTransactionExtraction } from "@/hooks/useTransactionExtraction";
import { useRouter } from "expo-router";
import { useTransactionStore } from "@/store/transactionStore";
import { launchCamera, launchImageLibrary } from "@/utils/imagePicker";

export default function ReceiptForm() {
const [selectedImage, setSelectedImage] =
Expand All @@ -24,40 +24,16 @@ export default function ReceiptForm() {
const { addBulkTransactions } = useTransactionStore();

const handleImagePick = async () => {
const permissionResult =
await ImagePicker.requestMediaLibraryPermissionsAsync();

if (!permissionResult.granted) {
Alert.alert("Permission to access camera roll is required!");
return;
}

const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
quality: 1,
allowsEditing: false,
aspect: [16, 9],
});

if (!result.canceled) {
setSelectedImage(result.assets[0]);
const result = await launchImageLibrary();
if (result.success) {
setSelectedImage(result.asset);
}
};

const handleCameraButtonPress = async () => {
const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
if (!permissionResult.granted) {
Alert.alert("Permission to access camera is required!");
return;
}
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ["images"],
quality: 1,
allowsEditing: false,
aspect: [16, 9],
});
if (!result.canceled) {
setSelectedImage(result.assets[0]);
const result = await launchCamera();
if (result.success) {
setSelectedImage(result.asset);
}
};

Expand Down
122 changes: 122 additions & 0 deletions utils/imagePicker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Image Picker Utilities
*
* Reusable functions for camera and gallery image selection with built-in
* permission handling and camera availability checks (iOS Simulator support).
*
* Usage:
* ```typescript
* import { launchCamera, launchImageLibrary } from "@/utils/imagePicker";
*
* // Pick from gallery
* const result = await launchImageLibrary();
* if (result.success) {
* console.log(result.asset.uri);
* }
*
* // Capture with camera
* const result = await launchCamera();
* if (result.success) {
* setImage(result.asset);
* }
* ```
*
* Note: Camera is not available on iOS Simulator. The `launchCamera` function
* will show an alert and return `{ success: false }` in that case.
*/

import * as ImagePicker from "expo-image-picker";
import { Alert } from "react-native";

export type ImagePickerResult = {
success: true;
asset: ImagePicker.ImagePickerAsset;
} | {
success: false;
};

/**
* Check if camera is available and show alert if not.
* Returns true if camera is available, false otherwise.
*/
export async function checkCameraAvailability(): Promise<boolean> {
const cameraStatus = await ImagePicker.getCameraPermissionsAsync();

if (!cameraStatus.canAskAgain && !cameraStatus.granted) {
Alert.alert(
"Camera Not Available",
"Camera is not available on this device. Please use the gallery option instead, or test on a physical device.",
[{ text: "OK" }]
);
return false;
}

return true;
}

/**
* Launch camera to capture an image.
* Handles permission requests and camera availability checks.
*/
export async function launchCamera(): Promise<ImagePickerResult> {
const isAvailable = await checkCameraAvailability();
if (!isAvailable) {
return { success: false };
}

const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
if (!permissionResult.granted) {
Alert.alert("Permission to access camera is required!");
return { success: false };
}

try {
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ["images"],
quality: 1,
allowsEditing: false,
aspect: [16, 9],
});

if (!result.canceled && result.assets[0]) {
return { success: true, asset: result.assets[0] };
}

return { success: false };
} catch (error) {
// Camera hardware not available (e.g., iOS Simulator)
Alert.alert(
"Camera Not Available",
"Camera is not available on this device. Please use the gallery option instead, or test on a physical device.",
[{ text: "OK" }]
);
return { success: false };
}
}

/**
* Launch image library to pick an image.
* Handles permission requests.
*/
export async function launchImageLibrary(): Promise<ImagePickerResult> {
const permissionResult =
await ImagePicker.requestMediaLibraryPermissionsAsync();

if (!permissionResult.granted) {
Alert.alert("Permission to access camera roll is required!");
return { success: false };
}

const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
quality: 1,
allowsEditing: false,
aspect: [16, 9],
});

if (!result.canceled && result.assets[0]) {
return { success: true, asset: result.assets[0] };
}

return { success: false };
}