import React, { useState } from "react"; import { View, ScrollView, Pressable, PermissionsAndroid, Platform, ActivityIndicator, } from "react-native"; import { Text } from "@/components/ui/text"; import { Card } from "@/components/ui/card"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { toast } from "@/lib/toast-store"; import { ArrowLeft, MessageSquare, RefreshCw } from "@/lib/icons"; import { useColorScheme } from "nativewind"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; // Installed via: npm install react-native-get-sms-android --legacy-peer-deps // Android only — iOS does not permit reading SMS let SmsAndroid: any = null; try { const smsModule = require("react-native-get-sms-android"); SmsAndroid = smsModule.default || smsModule; } catch (e) { console.log("[SMS] Module require failed:", e); } // Keywords to match Ethiopian banking SMS messages const BANK_KEYWORDS = ["CBE", "DashenBank", "Dashen", "127", "telebirr"]; interface SmsMessage { _id: string; address: string; body: string; date: number; date_sent: number; } interface ParsedPayment { smsId: string; bank: string; amount: string; ref: string; date: number; body: string; sender: string; } export default function SmsScanScreen() { const nav = useSirouRouter(); const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(false); const [scanned, setScanned] = useState(false); const scanSms = async () => { if (Platform.OS !== "android") { toast.error("Android Only", "SMS reading is only supported on Android."); return; } if (!SmsAndroid) { toast.error( "Native Module Error", "SMS scanning requires a Development Build. Expo Go does not support this package.", ); return; } setLoading(true); try { // Request SMS permission const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.READ_SMS, { title: "SMS Access Required", message: "Yaltopia needs access to read your banking SMS messages to match payments.", buttonPositive: "Allow", buttonNegative: "Deny", }, ); if (granted !== PermissionsAndroid.RESULTS.GRANTED) { toast.error("Permission Denied", "SMS access was not granted."); return; } // Only look at messages from the past 20 minutes const fiveMinutesAgo = Date.now() - 20 * 60 * 1000; const filter = { box: "inbox", minDate: fiveMinutesAgo, maxCount: 50, }; SmsAndroid.list( JSON.stringify(filter), (fail: string) => { console.error("[SMS] Failed to read:", fail); toast.error("Read Failed", "Could not read SMS messages."); setLoading(false); }, (count: number, smsList: string) => { const allMessages: SmsMessage[] = JSON.parse(smsList); // Filter for banking messages only const bankMessages = allMessages.filter((sms) => { const body = sms.body?.toUpperCase() || ""; const address = sms.address?.toUpperCase() || ""; return BANK_KEYWORDS.some( (kw) => body.includes(kw.toUpperCase()) || address.includes(kw.toUpperCase()), ); }); setMessages(bankMessages); setScanned(true); setLoading(false); if (bankMessages.length === 0) { toast.info( "No Matches", "No banking SMS found in the last 5 minutes.", ); } else { toast.success( "Found!", `${bankMessages.length} banking message(s) detected.`, ); } }, ); } catch (err: any) { console.error("[SMS] Error:", err); toast.error("Error", err.message); setLoading(false); } }; const formatTime = (timestamp: number) => { const date = new Date(timestamp); return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }; const parseMessage = (sms: SmsMessage): ParsedPayment | null => { const body = sms.body; const addr = sms.address.toUpperCase(); const text = (body + addr).toUpperCase(); let bank = "Unknown"; let amount = ""; let ref = ""; // CBE Patterns if (text.includes("CBE") || addr === "CBE") { bank = "CBE"; // Pattern: "ETB 1,234.56" or "ETB 1,234" const amtMatch = body.match(/ETB\s*([\d,.]+)/i); if (amtMatch) amount = amtMatch[1]; // Pattern: "Ref: 123456789" const refMatch = body.match(/Ref:?\s*(\w+)/i); if (refMatch) ref = refMatch[1]; } // Telebirr Patterns else if (text.includes("TELEBIRR") || addr === "TELEBIRR") { bank = "Telebirr"; // Pattern: "Birr 1,234.56" const amtMatch = body.match(/Birr\s*([\d,.]+)/i); if (amtMatch) amount = amtMatch[1]; // Pattern: "Trans ID: 12345678" const refMatch = body.match(/Trans ID:?\s*(\w+)/i); if (refMatch) ref = refMatch[1]; } // Dashen Patterns else if (text.includes("DASHEN") || addr === "DASHEN") { bank = "Dashen"; // Pattern: "ETB 1,234.56" const amtMatch = body.match(/ETB\s*([\d,.]+)/i); if (amtMatch) amount = amtMatch[1]; // Pattern: "Reference No: 12345678" const refMatch = body.match(/(?:Ref(?:erence)?(?:\s*No)?):?\s*(\w+)/i); if (refMatch) ref = refMatch[1]; } if (bank === "Unknown") return null; return { smsId: sms._id, bank, amount, ref, date: sms.date, body: sms.body, sender: sms.address, }; }; const getBankColor = (bank: string) => { switch (bank) { case "CBE": return "#16a34a"; case "Telebirr": return "#7c3aed"; case "Dashen": return "#1d4ed8"; default: return "#ea580c"; } }; return ( {/* Header */} nav.back()} className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border" > Scan SMS {/* Spacer */} Scan SMS Finds banking messages from the last 5 minutes {/* Scan Button */} {loading ? ( ) : ( <> {scanned ? "Scan Again" : "Scan Now"} )} {!scanned && !loading && ( Tap "Scan Now" to search for CBE, Dashen Bank, and Telebirr messages from the last 5 minutes. )} {scanned && messages.length === 0 && ( No banking messages found in the last 5 minutes. )} {messages.map((sms) => { const parsed = parseMessage(sms); if (!parsed) return null; const bankColor = getBankColor(parsed.bank); return ( {parsed.bank} {formatTime(sms.date)} {/* Extracted Data */} {parsed.amount ? ( Amount ETB {parsed.amount} ) : null} {parsed.ref ? ( Reference {parsed.ref} ) : null} "{sms.body}" ); })} ); }