import React, { useEffect, useState } from "react";
import {
View,
Pressable,
TextInput,
StyleSheet,
ActivityIndicator,
Switch,
Platform,
} from "react-native";
import { useColorScheme } from "nativewind";
import { Text } from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import {
ArrowLeft,
Calendar,
ChevronDown,
DollarSign,
Send,
CalendarSearch,
Clock,
User,
Phone,
Building2,
Hash,
Banknote,
Upload,
} from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { useSirouRouter } from "@sirou/react-native";
import { AppRoutes } from "@/lib/routes";
import { api, BASE_URL } from "@/lib/api";
import { toast } from "@/lib/toast-store";
import { useAuthStore } from "@/lib/auth-store";
import * as ImagePicker from "expo-image-picker";
import { PickerModal, SelectOption } from "@/components/PickerModal";
import { CalendarGrid } from "@/components/CalendarGrid";
import { TimerPickerModal } from "react-native-timer-picker";
import { LinearGradient } from "expo-linear-gradient";
import { getPlaceholderColor } from "@/lib/colors";
import { getScanData } from "@/lib/scan-cache";
import { FormFlow } from "@/components/FormFlow";
const S = StyleSheet.create({
input: {
paddingHorizontal: 12,
paddingVertical: 12,
fontSize: 12,
fontWeight: "500",
borderRadius: 6,
borderWidth: 1,
textAlignVertical: "center",
},
inputCenter: {
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 14,
fontWeight: "500",
borderRadius: 6,
borderWidth: 1,
textAlign: "center",
textAlignVertical: "center",
},
});
function useInputColors() {
const { colorScheme } = useColorScheme();
const dark = colorScheme === "dark";
return {
bg: dark ? "rgba(30,30,30,0.8)" : "rgba(241,245,249,0.2)",
border: dark ? "rgba(255,255,255,0.08)" : "rgba(203,213,225,0.6)",
text: dark ? "#f1f5f9" : "#0f172a",
placeholder: "rgba(100,116,139,0.45)",
};
}
function Field({
label,
value,
onChangeText,
placeholder,
numeric = false,
center = false,
flex,
multiline = false,
}: {
label: string;
value: string;
onChangeText: (v: string) => void;
placeholder: string;
numeric?: boolean;
center?: boolean;
flex?: number;
multiline?: boolean;
}) {
const c = useInputColors();
return (
{label}
);
}
function Label({ children }: { children: string }) {
return (
{children}
);
}
const currencies = ["ETB", "USD", "EUR", "GBP", "KES", "ZAR"];
const paymentMethods = [
"Telebirr",
"CBE",
"Dashen",
"DECSI",
"Bank Transfer",
"Cash",
"Other",
];
const providers = ["telebirr", "cbe", "dashen", "decsi", "other"];
const STEPS = [
{ key: "payment", label: "Payment Details" },
{ key: "transaction", label: "Transaction" },
{ key: "merchant", label: "Merchant" },
{ key: "sender", label: "Sender" },
{ key: "verification", label: "Verification" },
{ key: "summary", label: "Summary" },
];
export default function AddReceiptScreen() {
const nav = useSirouRouter();
const [step, setStep] = useState(0);
const [submitting, setSubmitting] = useState(false);
const [scanning, setScanning] = useState(false);
const [scanFailures, setScanFailures] = useState(0);
const token = useAuthStore((s) => s.token);
const [amount, setAmount] = useState("");
const [currency, setCurrency] = useState("ETB");
const [paymentDate, setPaymentDate] = useState(
new Date().toISOString().split("T")[0],
);
const [paymentTime, setPaymentTime] = useState(
new Date().toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
}),
);
const [paymentMethod, setPaymentMethod] = useState("Telebirr");
const [transactionId, setTransactionId] = useState("");
const [referenceNumber, setReferenceNumber] = useState("");
const [merchantName, setMerchantName] = useState("");
const [merchantId, setMerchantId] = useState("");
const [provider, setProvider] = useState("telebirr");
const [senderName, setSenderName] = useState("");
const [senderPhone, setSenderPhone] = useState("");
const [verifyWithProvider, setVerifyWithProvider] = useState(false);
const [verifyWithVerifierApi, setVerifyWithVerifierApi] = useState(false);
const [showCurrency, setShowCurrency] = useState(false);
const [showPaymentMethod, setShowPaymentMethod] = useState(false);
const [showProvider, setShowProvider] = useState(false);
const [showPaymentDate, setShowPaymentDate] = useState(false);
const [showPaymentTime, setShowPaymentTime] = useState(false);
const { colorScheme } = useColorScheme();
const isDark = colorScheme === "dark";
const c = useInputColors();
useEffect(() => {
const scanData = getScanData();
if (!scanData) return;
if (scanData.amount != null) setAmount(String(scanData.amount));
if (scanData.currency) setCurrency(scanData.currency);
if (scanData.paymentDate) {
try {
setPaymentDate(
new Date(scanData.paymentDate).toISOString().split("T")[0],
);
} catch (_) {}
}
if (scanData.paymentTime) setPaymentTime(scanData.paymentTime);
if (scanData.paymentMethod) setPaymentMethod(scanData.paymentMethod);
if (scanData.transactionId) setTransactionId(scanData.transactionId);
if (scanData.referenceNumber) setReferenceNumber(scanData.referenceNumber);
if (scanData.merchantName) setMerchantName(scanData.merchantName);
if (scanData.merchantId) setMerchantId(scanData.merchantId);
if (scanData.provider) setProvider(scanData.provider);
if (scanData.senderName) setSenderName(scanData.senderName);
if (scanData.senderPhone) setSenderPhone(scanData.senderPhone.replace(/^\+251|\++/g, ""));
}, []);
const handleNext = () => {
if (step === 0) {
if (!amount || parseFloat(amount) <= 0) {
toast.error(
"Validation Error",
"Amount is required and must be greater than 0",
);
return;
}
}
setStep((s) => s + 1);
};
const handleSubmit = async () => {
if (!amount || parseFloat(amount) <= 0) {
toast.error(
"Validation Error",
"Amount is required and must be greater than 0",
);
throw new Error("Amount is required");
}
if (!transactionId) {
toast.error("Validation Error", "Transaction ID is required");
throw new Error("Transaction ID is required");
}
setSubmitting(true);
try {
const payload = {
amount: parseFloat(amount),
currency,
paymentDate,
paymentTime,
paymentMethod,
transactionId,
referenceNumber,
merchantName,
merchantId,
provider,
senderName,
senderPhone: senderPhone ? (senderPhone.startsWith("+") ? senderPhone : `+251${senderPhone}`) : undefined,
verifyWithProvider,
verifyWithVerifierApi,
};
await api.scan.paymentReceiptManual({ body: payload });
toast.success("Success", "Receipt added successfully!");
nav.back();
} catch (error: any) {
console.error("[AddReceipt] Error:", error);
const msg =
error?.response?.data?.message ||
error?.data?.message ||
error?.message ||
"Failed to add receipt";
toast.error("Error", msg);
throw error;
} finally {
setSubmitting(false);
}
};
const handlePickImage = async () => {
try {
const { status } =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== "granted") {
toast.error(
"Permission Denied",
"We need access to your gallery to upload receipts.",
);
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
quality: 0.8,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
const uri = result.assets[0].uri;
await handleProcessImage(uri);
}
} catch (e: any) {
console.error("[AddReceipt] Pick Image Error:", e);
toast.error("Picker Failed", "Could not launch gallery picker.");
}
};
const handleProcessImage = async (uri: string) => {
setScanning(true);
toast.info("Processing...", "Uploading receipt to AI extraction engine.");
try {
const formData = new FormData();
const fileExt = uri.split(".").pop() || "jpg";
const fileName = `receipt-${Date.now()}.${fileExt}`;
const type = `image/${fileExt === "jpg" ? "jpeg" : fileExt}`;
formData.append("file", {
uri: Platform.OS === "android" ? uri : uri.replace("file://", ""),
name: fileName,
type: type,
} as any);
const response = await fetch(`${BASE_URL}scan/payment-receipt`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
body: formData,
});
if (!response.ok) {
const err = await response
.json()
.catch(() => ({ message: "Scan processing failed." }));
throw new Error(err.message || "AI extraction failed.");
}
const scanResult = await response.json();
if (!scanResult.success) {
throw new Error(
scanResult.message || "AI extraction was unsuccessful.",
);
}
toast.success("Success!", "Data extracted successfully.");
const ocr = scanResult.data || {};
if (ocr.amount != null) setAmount(String(ocr.amount));
if (ocr.currency) setCurrency(ocr.currency);
if (ocr.paymentDate) {
try {
setPaymentDate(new Date(ocr.paymentDate).toISOString().split("T")[0]);
} catch (_) {}
}
if (ocr.paymentTime) setPaymentTime(ocr.paymentTime);
if (ocr.paymentMethod) setPaymentMethod(ocr.paymentMethod);
if (ocr.transactionId) setTransactionId(ocr.transactionId);
if (ocr.referenceNumber) setReferenceNumber(ocr.referenceNumber);
if (ocr.merchantName) setMerchantName(ocr.merchantName);
if (ocr.merchantId) setMerchantId(ocr.merchantId);
if (ocr.provider) setProvider(ocr.provider);
if (ocr.senderName) setSenderName(ocr.senderName);
if (ocr.senderPhone) setSenderPhone(ocr.senderPhone.replace(/^\+251|\++/g, ""));
try {
await handleSubmit();
} catch {
const nextCount = scanFailures + 1;
setScanFailures(nextCount);
if (nextCount >= 2) {
toast.info("Scan failed, fill details below", "");
} else {
toast.error("Extraction failed, try again", "");
}
}
} catch (err: any) {
console.error("[AddReceipt] Extraction Error:", err);
const nextCount = scanFailures + 1;
setScanFailures(nextCount);
if (nextCount >= 2) {
toast.info("Scan failed, fill details below", "");
} else {
toast.error("Extraction failed, try again", "");
}
} finally {
setScanning(false);
}
};
const formattedAmount = (parseFloat(amount) || 0).toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
return (
setStep(step - 1)}
onComplete={handleSubmit}
loading={submitting}
completeLabel="Add Receipt"
>
{step === 0 && (
<>
{scanning ? (
) : (
)}
Scan from Gallery
Upload image to auto-fill form
Currency
setShowCurrency(true)}
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
style={{ backgroundColor: c.bg, borderColor: c.border }}
>
{currency}
Payment Date
setShowPaymentDate(true)}
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
style={{ backgroundColor: c.bg, borderColor: c.border }}
>
{paymentDate}
Payment Time
setShowPaymentTime(true)}
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
style={{ backgroundColor: c.bg, borderColor: c.border }}
>
{paymentTime || "Select"}
Payment Method
setShowPaymentMethod(true)}
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
style={{ backgroundColor: c.bg, borderColor: c.border }}
>
{paymentMethod}
Provider
setShowProvider(true)}
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
style={{ backgroundColor: c.bg, borderColor: c.border }}
>
{provider}
>
)}
{step === 1 && (
<>
>
)}
{step === 2 && (
<>
>
)}
{step === 3 && (
<>
Sender Phone
+251
>
)}
{step === 4 && (
<>
Verify with Provider
Check payment status with the provider
Verify with Verifier API
Run verification through the verifier service
>
)}
{step === 5 && (
<>
Amount
{currency} {formattedAmount}
Payment Date
{paymentDate}
Payment Time
{paymentTime}
Payment Method
{paymentMethod}
Provider
{provider}
Transaction ID
{transactionId}
{referenceNumber ? (
Reference
{referenceNumber}
) : null}
{merchantName ? (
Merchant
{merchantName}
) : null}
{merchantId ? (
Merchant ID
{merchantId}
) : null}
{senderName ? (
Sender
{senderName}
) : null}
{senderPhone ? (
Sender Phone
+251{senderPhone}
) : null}
Total
{currency} {formattedAmount}
>
)}
setShowCurrency(false)}
title="Select Currency"
>
{currencies.map((curr) => (
{
setCurrency(v);
setShowCurrency(false);
}}
/>
))}
setShowPaymentMethod(false)}
title="Select Payment Method"
>
{paymentMethods.map((method) => (
{
setPaymentMethod(v);
setShowPaymentMethod(false);
}}
/>
))}
setShowProvider(false)}
title="Select Provider"
>
{providers.map((prov) => (
{
setProvider(v);
setShowProvider(false);
}}
/>
))}
setShowPaymentDate(false)}
title="Select Payment Date"
>
{
setPaymentDate(v);
setShowPaymentDate(false);
}}
/>
setShowPaymentTime(false)}
onConfirm={(pickedDuration) => {
const hours = String(pickedDuration.hours ?? 0).padStart(2, "0");
const minutes = String(pickedDuration.minutes ?? 0).padStart(2, "0");
setPaymentTime(`${hours}:${minutes}`);
setShowPaymentTime(false);
}}
styles={{
theme: isDark ? "dark" : "light",
modalTitle: {
fontSize: 18,
fontWeight: "700",
},
contentContainer: {
width: "80%",
marginHorizontal: 16,
},
confirmButton: {
borderRadius: 8,
paddingHorizontal: 40,
},
cancelButton: {
borderRadius: 8,
borderWidth: 1,
borderColor: "#EDD5D1",
paddingHorizontal: 40,
},
}}
hideSeconds
/>
);
}