257 lines
8.0 KiB
TypeScript
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>
|
|
);
|
|
}
|