Yaltopia-Tickets-App/app/sms-scan.tsx

257 lines
8.0 KiB
TypeScript

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 {
SmsAndroid = require("react-native-get-sms-android").default;
} catch (_) {}
// 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;
}
export default function SmsScanScreen() {
const nav = useSirouRouter<AppRoutes>();
const { colorScheme } = useColorScheme();
const isDark = colorScheme === "dark";
const [messages, setMessages] = useState<SmsMessage[]>([]);
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(
"Package Missing",
"Run: npm install react-native-get-sms-android",
);
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 5 minutes
const fiveMinutesAgo = Date.now() - 5 * 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 getBankLabel = (sms: SmsMessage) => {
const text = (sms.body + sms.address).toUpperCase();
if (text.includes("CBE")) return { name: "CBE", color: "#16a34a" };
if (text.includes("DASHEN"))
return { name: "Dashen Bank", color: "#1d4ed8" };
if (text.includes("127") || text.includes("TELEBIRR"))
return { name: "Telebirr", color: "#7c3aed" };
return { name: "Bank", color: "#ea580c" };
};
return (
<ScreenWrapper className="bg-background">
{/* Header */}
<View className="px-6 pt-4 flex-row justify-between items-center">
<Pressable
onPress={() => nav.back()}
className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border"
>
<ArrowLeft color={isDark ? "#fff" : "#0f172a"} size={20} />
</Pressable>
<Text variant="h4" className="text-foreground font-semibold">
Scan SMS
</Text>
<View className="w-10" /> {/* Spacer */}
</View>
<View className="px-5 pt-6 pb-4">
<Text variant="h3" className="text-foreground font-bold">
Scan SMS
</Text>
<Text variant="muted" className="mt-1">
Finds banking messages from the last 5 minutes
</Text>
</View>
{/* Scan Button */}
<View className="px-5 mb-4">
<Pressable
onPress={scanSms}
disabled={loading}
className="h-12 rounded-[10px] bg-primary items-center justify-center flex-row gap-2"
>
{loading ? (
<ActivityIndicator color="white" />
) : (
<>
<RefreshCw color="white" size={16} />
<Text className="text-white font-bold uppercase tracking-widest text-xs">
{scanned ? "Scan Again" : "Scan Now"}
</Text>
</>
)}
</Pressable>
</View>
<ScrollView
className="flex-1 px-5"
contentContainerStyle={{ paddingBottom: 150 }}
showsVerticalScrollIndicator={false}
>
{!scanned && !loading && (
<View className="flex-1 items-center justify-center py-20 gap-4">
<View className="bg-primary/10 p-6 rounded-[24px]">
<MessageSquare
size={40}
className="text-primary"
color="#ea580c"
strokeWidth={1.5}
/>
</View>
<Text variant="muted" className="text-center px-10">
Tap "Scan Now" to search for CBE, Dashen Bank, and Telebirr
messages from the last 5 minutes.
</Text>
</View>
)}
{scanned && messages.length === 0 && (
<View className="flex-1 items-center justify-center py-20 gap-4">
<Text variant="muted" className="text-center">
No banking messages found in the last 5 minutes.
</Text>
</View>
)}
<View className="gap-3">
{messages.map((sms) => {
const bank = getBankLabel(sms);
return (
<Card key={sms._id} className="rounded-[12px] bg-card p-4">
<View className="flex-row items-center justify-between mb-2">
<View
className="px-3 py-1 rounded-full"
style={{ backgroundColor: bank.color + "20" }}
>
<Text
className="text-xs font-bold"
style={{ color: bank.color }}
>
{bank.name}
</Text>
</View>
<Text variant="muted" className="text-xs">
{formatTime(sms.date)}
</Text>
</View>
<Text className="text-foreground text-sm leading-5">
{sms.body}
</Text>
<Text variant="muted" className="text-xs mt-2">
From: {sms.address}
</Text>
</Card>
);
})}
</View>
</ScrollView>
</ScreenWrapper>
);
}