import * as ImagePicker from "expo-image-picker"; import { Alert } from "react-native"; import { applyReceiptOcrToForm, recognizeReceiptFromImage, } from "@/lib/receipt-ocr"; import type { ReceiptLineItem } from "@/lib/receipt-parse"; export type ReceiptSuggestFn = (input: { text: string }) => Promise<{ amount: number | null; date: Date | null; description: string | null; }>; export type PickedReceiptImage = { uri: string; base64: string; filename: string; mimeType: string; }; export type ReceiptScanResult = { image: PickedReceiptImage; description: string; amountText: string; date: Date; ocrText: string; items: ReceiptLineItem[]; subtotal: number | null; tax: number | null; total: number | null; }; async function requestPermissions(fromCamera: boolean): Promise { const permission = fromCamera ? await ImagePicker.requestCameraPermissionsAsync() : await ImagePicker.requestMediaLibraryPermissionsAsync(); if (permission.granted) return true; Alert.alert( fromCamera ? "Camera access needed" : "Photos access needed", fromCamera ? "Allow camera access to scan receipts." : "Allow photo library access to import receipt images.", ); return false; } export async function pickReceiptImage( fromCamera: boolean, ): Promise { if (!(await requestPermissions(fromCamera))) return null; const result = fromCamera ? await ImagePicker.launchCameraAsync({ mediaTypes: ["images"], quality: 0.9, base64: true, }) : await ImagePicker.launchImageLibraryAsync({ mediaTypes: ["images"], quality: 0.9, base64: true, }); if (result.canceled || !result.assets[0]) return null; const asset = result.assets[0]; if (!asset.uri || !asset.base64) { Alert.alert( "Could not read image", "Try another photo or lower the image size.", ); return null; } return { uri: asset.uri, base64: asset.base64, filename: asset.fileName ?? (fromCamera ? "receipt.jpg" : "receipt-import.jpg"), mimeType: asset.mimeType ?? "image/jpeg", }; } export async function scanReceiptImage( fromCamera: boolean, current: { description: string; amountText: string; date: Date }, suggest?: ReceiptSuggestFn, ): Promise { const image = await pickReceiptImage(fromCamera); if (!image) return null; try { const ocr = await recognizeReceiptFromImage(image.uri); let next = applyReceiptOcrToForm(ocr, current); next = { ...next, ocrText: ocr.rawText || next.ocrText }; if (ocr.rawText && suggest) { try { const suggestion = await suggest({ text: ocr.rawText }); next = { description: suggestion.description?.trim() || next.description, amountText: suggestion.amount != null ? String(suggestion.amount) : next.amountText, date: suggestion.date ? new Date(suggestion.date) : next.date, ocrText: ocr.rawText, }; } catch { // Local parse is enough when server suggest fails offline. } } return { image, ...next, items: ocr.items, subtotal: ocr.subtotal, tax: ocr.tax, total: ocr.amount, }; } catch (error) { Alert.alert( "OCR failed", error instanceof Error ? error.message : "Could not read text from the receipt.", ); return null; } }