Commit b72847bb authored by alep's avatar alep

merge with master

parents 94f91fc0 c6c3917f
......@@ -15,7 +15,7 @@ import {
AppState,
} from 'react-native';
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import AsyncStorage from '@react-native-async-storage/async-storage';
// import AsyncStorage from '@react-native-async-storage/async-storage';
import DeviceInfo from 'react-native-device-info';
const { ContactModule, VIPStorage ,OverlayPermission } = NativeModules as any;
......@@ -25,34 +25,71 @@ type Contact = { name: string; number: string };
const CONTACTS_KEY = 'whoscallContacts';
const permissionsList = [
const permissionsList = Platform.select({
android: [
{ key: 'READ_PHONE_STATE', android: PERMISSIONS.ANDROID.READ_PHONE_STATE },
{ key: 'READ_CALL_LOG', android: PERMISSIONS.ANDROID.READ_CALL_LOG },
{ key: 'Display Over Other Apps', android: null }, // overlay special case
];
function normalize(n: string) {
return (n || '').replace(/[^+0-9]/g, '');
}
async function loadContacts(): Promise<Contact[]> {
const json = await AsyncStorage.getItem(CONTACTS_KEY);
return json ? JSON.parse(json) : [];
}
async function persistContacts(contacts: Contact[]) {
const json = JSON.stringify(contacts);
await AsyncStorage.setItem(CONTACTS_KEY, json);
// Sync to native so IncomingCallService can resolve when JS isn’t active
if (VIPStorage?.setContactsJson) {
],
ios: [{ key: 'Call Directory Extension', ios: true }],
})!;
const { OverlayPermission } = NativeModules;
type CallDirectoryManagerType = {
getEnabledStatusForExtension(identifier: string): Promise<number>;
reloadExtension(identifier: string): Promise<boolean>;
};
const CallDirectoryManager: CallDirectoryManagerType | undefined = NativeModules.CallDirectoryManager;
// async function saveVIPNumber(name: string, number: string) {
// try {
// await AsyncStorage.setItem('vipName', name);
// await AsyncStorage.setItem('vipNumber', number);
// const {VIPStorage} = NativeModules;
// if (!VIPStorage) {
// console.error("❌ VIPStorage is undefined. Did you register StoragePackage in MainApplication?");
// return;
// }
// await VIPStorage.saveVIP(name, number);
// console.log('VIPStorage from NativeModules:', NativeModules.VIPStorage);
// } catch (error) {
// console.error('Failed to save VIP info:', error);
// }
// }
async function checkOverlayPermission() {
if (Platform.OS === 'android') {
try {
await VIPStorage.setContactsJson(json);
const granted: boolean = await OverlayPermission.hasPermission();
if (!granted) {
Alert.alert(
'Overlay Permission Required',
'This app needs permission to display over other apps.',
[{ text: 'Open Settings', onPress: openOverlaySettings }]
);
}
return granted;
} catch (e) {
console.warn('VIPStorage.setContactsJson failed:', e);
console.error('Error checking overlay permission:', e);
return false;
}
} else {
return true; // iOS doesn't require this
}
}
const openOverlaySettings = async () => {
if (Platform.OS === 'android') {
const pkg = DeviceInfo.getBundleId(); // gets your app package name
try {
await Linking.openSettings(); // fallback
await Linking.openURL(`package:${pkg}`);
} catch (err) {
console.warn('Failed to open overlay settings:', err);
}
}
};
export default function App() {
const [statuses, setStatuses] = useState<Record<string, string>>({});
const [contacts, setContacts] = useState<Contact[]>([]);
......@@ -107,25 +144,37 @@ const openOverlaySettings = async () => {
const checkAllPermissions = useCallback(async () => {
const newStatuses: Record<string, string> = {};
for (const perm of permissionsList) {
if (perm.key === 'Display Over Other Apps') {
// ✅ Ask native for the real status
if (Platform.OS === 'ios') {
try {
const allowed = await OverlayPermission.hasOverlayPermission();
newStatuses[perm.key] = RESULTS_BOOL[String(allowed) as 'true' | 'false'];
} catch {
newStatuses[perm.key] = RESULTS.DENIED;
if (CallDirectoryManager) {
const status = await CallDirectoryManager.getEnabledStatusForExtension(
"com.whoscall.whosallDirectoryExtension"
);
newStatuses['Call Directory Extension'] = status === 2 ? RESULTS.GRANTED : RESULTS.DENIED;
} else {
console.warn("⚠️ CallDirectoryManager native module not available");
newStatuses['Call Directory Extension'] = RESULTS.DENIED;
}
} catch (e) {
console.error("Error checking Call Directory extension:", e);
newStatuses['Call Directory Extension'] = RESULTS.DENIED;
}
} else {
for (const perm of permissionsList) {
if (perm.key === 'Display Over Other Apps') {
const granted = await checkOverlayPermission();
newStatuses[perm.key] = granted ? RESULTS.GRANTED : RESULTS.DENIED;
continue;
}
const result = await check(perm.android!);
if ('android' in perm && perm.android) {
const result = await check(perm.android);
newStatuses[perm.key] = result;
}
}
setStatuses(newStatuses);
// Request any missing ones (overlay requires manual settings)
for (const perm of permissionsList) {
if (newStatuses[perm.key] !== RESULTS.GRANTED) {
if (perm.key === 'Display Over Other Apps') {
......@@ -134,173 +183,117 @@ const checkAllPermissions = useCallback(async () => {
'This app needs permission to display over other apps.',
[{ text: 'Open Settings', onPress: openOverlaySettings }]
);
} else {
const result = await request(perm.android!);
} else if ('android' in perm && perm.android) {
const result = await request(perm.android);
setStatuses(prev => ({ ...prev, [perm.key]: result }));
}
}
}
}, []);
useEffect(() => {
const sub = AppState.addEventListener('change', (state) => {
if (state === 'active') checkAllPermissions();
});
return () => sub.remove();
}, [checkAllPermissions]);
// (Optional) Guard the old event flow so it won't crash if method not present
useEffect(() => {
const subscription = eventEmitter.addListener('resolveContact', async (incomingNumber: string) => {
}
if (Platform.OS === 'ios') {
try {
const json = await AsyncStorage.getItem(CONTACTS_KEY);
if (!json) return;
if (CallDirectoryManager) {
const status = await CallDirectoryManager.getEnabledStatusForExtension(
"com.whoscall.whoscallDirectoryExtension"
);
let mapped: typeof RESULTS[keyof typeof RESULTS]; // tells TS it's a valid RESULTS value
const displayName = await ContactModule.getDisplayName(json, incomingNumber);
if (ContactModule?.sendResolvedContact) {
ContactModule.sendResolvedContact(incomingNumber, displayName || incomingNumber);
if (status === 2) {
mapped = RESULTS.GRANTED; // enabled
} else if (status === 1) {
mapped = RESULTS.DENIED; // disabled by user
} else {
mapped = RESULTS.UNAVAILABLE; // unknown / not supported
}
newStatuses['Call Directory Extension'] = mapped;
} else {
console.warn("⚠️ CallDirectoryManager native module not available");
newStatuses['Call Directory Extension'] = RESULTS.DENIED;
Alert.alert(
"Enable Caller ID",
"Go to Settings → Phone → Call Blocking & Identification and enable [whoscall] to see caller ID labels.",
[
{ text: "Open Settings", onPress: () => Linking.openURL("app-settings:") },
{ text: "Cancel", style: "cancel" }
]
);
}
} catch (e) {
console.error("Error checking Call Directory extension:", e);
newStatuses['Call Directory Extension'] = RESULTS.DENIED;
Alert.alert(
"Enable Caller ID",
"Go to Settings → Phone → Call Blocking & Identification and enable [Your App] to see caller ID labels.",
[
{ text: "Open Settings", onPress: () => Linking.openURL("app-settings:") },
{ text: "Cancel", style: "cancel" }
]
);
}
} catch (err) {
console.error('Failed to resolve contact:', err);
}
});
return () => subscription.remove();
}, []);
useEffect(() => {
console.log('PermissionsScreen mounted');
console.log('All NativeModules keys:', Object.keys(NativeModules));
console.log('VIPStorage:', VIPStorage);
// Initial load
refreshContacts();
checkAllPermissions();
}, [checkAllPermissions, refreshContacts]);
console.log("PermissionsScreen mounted");
console.log("All NativeModules keys:", Object.keys(NativeModules));
// console.log("VIPStorage:", NativeModules.VIPStorage);
// saveVIPNumber('VIP User', '+60123456789');
checkAllPermissions();
},[checkAllPermissions]);
const allGranted = Object.values(statuses).every(s => s === RESULTS.GRANTED);
// ---- Overlay settings opener (kept) ----
// const openOverlaySettings = async () => {
// if (Platform.OS === 'android') {
// const pkg = DeviceInfo.getBundleId(); // gets your app package name
// const url = `package:${pkg}`;
// --- iOS: Call Directory management ---
// const checkCallDirectoryStatus = async () => {
// if (Platform.OS !== "ios" || !CallDirectoryManager) {
// console.warn("CallDirectoryManager not available on this platform");
// return;
// }
// try {
// await Linking.openSettings(); // fallback
// await Linking.openURL(`android.settings.action.MANAGE_OVERLAY_PERMISSION/${url}`);
// } catch (err) {
// console.warn('Failed to open overlay settings:', err);
// const status = await CallDirectoryManager.getEnabledStatusForExtension(
// "com.whoscall.whoscallDirectoryExtension" // <-- must match your extension bundle ID
// );
// console.log("Call Directory status:", status);
// if (status === 2) {
// Alert.alert("✅ Enabled", "Call Directory Extension is enabled");
// } else {
// Alert.alert(
// "⚠️ Not Enabled",
// "Go to Settings → Phone → Call Blocking & Identification and enable Whoscall."
// );
// }
// } catch (e) {
// console.error("Error checking Call Directory status:", e);
// }
// };
const allGranted = Object.values(statuses).every(s => s === RESULTS.GRANTED);
// ---- Contacts CRUD ----
const onAddOrUpdate = async () => {
const trimmedName = name.trim();
const trimmedNum = number.trim();
if (!trimmedName) {
Alert.alert('Validation', 'Please enter a name.');
const reloadCallDirectory = async () => {
if (Platform.OS !== "ios" || !CallDirectoryManager) {
console.warn("CallDirectoryManager not available on this platform");
return;
}
if (!trimmedNum) {
Alert.alert('Validation', 'Please enter a number.');
return;
try {
const success = await CallDirectoryManager.reloadExtension(
"com.whoscall.whoscallDirectoryExtension"
);
console.log("Reload result:", success);
if (success) {
Alert.alert("🔄 Reloaded", "Extension reloaded successfully");
}
const normalized = normalize(trimmedNum);
const next = [...contacts];
const idx = next.findIndex(c => normalize(c.number) === normalized);
const newContact = { name: trimmedName, number: trimmedNum };
if (idx >= 0) next[idx] = newContact; else next.push(newContact);
await persistContacts(next);
setContacts(next);
setName('');
setNumber('');
};
const onDelete = async (target: Contact) => {
const targetNorm = normalize(target.number);
const next = contacts.filter(c => normalize(c.number) !== targetNorm);
await persistContacts(next);
setContacts(next);
};
const onClearAll = async () => {
Alert.alert('Clear All', 'Remove all contacts?', [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Clear',
style: 'destructive',
onPress: async () => {
await AsyncStorage.removeItem(CONTACTS_KEY);
if (VIPStorage?.setContactsJson) {
try { await VIPStorage.setContactsJson('[]'); } catch {}
} catch (e) {
console.error("Error reloading Call Directory:", e);
Alert.alert("❌ Reload Failed", String(e));
}
setContacts([]);
},
},
]);
};
const renderItem = ({ item }: { item: Contact }) => (
<View style={styles.itemRow}>
<View style={{ flex: 1 }}>
<Text style={styles.itemName}>{item.name}</Text>
<Text style={styles.itemNumber}>{item.number}</Text>
</View>
<TouchableOpacity style={styles.deleteBtn} onPress={() => onDelete(item)}>
<Text style={styles.deleteTxt}>Delete</Text>
</TouchableOpacity>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.header}>Whoscall Contacts</Text>
{/* Add / Update form */}
<View style={styles.form}>
<Text>Name :</Text>
<TextInput
value={name}
onChangeText={setName}
placeholder="Contact name"
style={styles.input}
autoCapitalize="words"
/>
<Text>Number phone :</Text>
<TextInput
value={number}
onChangeText={setNumber}
placeholder="Phone number (e.g., +60123456789)"
style={styles.input}
keyboardType="phone-pad"
/>
<View style={{ flexDirection: 'row', gap: 12 }}>
<Button title="Save Contact" onPress={onAddOrUpdate} />
<Button title="Clear All" color="#c62828" onPress={onClearAll} />
</View>
</View>
{/* Contact list */}
<Text style={styles.subheader}>Saved Contacts ({contacts.length})</Text>
<FlatList
data={contacts}
keyExtractor={(item, idx) => `${normalize(item.number)}_${idx}`}
renderItem={renderItem}
ItemSeparatorComponent={() => <View style={styles.separator} />}
style={{ flexGrow: 0, maxHeight: 260 }}
ListEmptyComponent={<Text style={styles.empty}>No contacts yet. Add one above.</Text>}
/>
{/* Permissions status */}
<Text style={styles.subheader}>Permissions Status</Text>
{permissionsList.map(perm => (
<View key={perm.key} style={styles.permRow}>
<Text style={styles.permKey}>{perm.key}</Text>
<Text style={styles.title}>Permissions Status</Text>
{(permissionsList ?? []).map(perm => (
<View key={perm.key} style={styles.row}>
<Text style={styles.text}>{perm.key}</Text>
<Text
style={[
styles.permVal,
......@@ -310,9 +303,13 @@ const checkAllPermissions = useCallback(async () => {
</Text>
</View>
))}
{allGranted && <Text style={styles.granted}>All permissions granted ✅</Text>}
<View style={{ height: 12 }} />
{Platform.OS === "android" && allGranted && <Text style={styles.granted}>All permissions granted ✅</Text>}
<Button title="Check Again" onPress={checkAllPermissions} />
{Platform.OS === "ios" && (
<>
<Button title="Reload Call Directory" onPress={reloadCallDirectory} />
</>
)}
</View>
);
}
......
......@@ -23,8 +23,8 @@ class IncomingCallService : CallScreeningService() {
// Lookup VIPs
// val prefs = getSharedPreferences("WhoscallPrefs", MODE_PRIVATE)
val vipNumber = "+60108222562"
val vipName = "Arvin"
val vipNumber = "+60164034061"
val vipName = "Alyp"
Log.d("IncomingCallReceiver", "Looking up number: $number, Found VIPNumber: $vipNumber, Found VIPName: $vipName")
// Only show overlay if VIP
......
......@@ -8,6 +8,7 @@ import android.provider.Settings
import android.app.role.RoleManager
import androidx.activity.result.contract.ActivityResultContracts
import com.facebook.react.ReactActivity
import android.os.Bundle
class MainActivity : ReactActivity() {
......@@ -35,10 +36,23 @@ class MainActivity : ReactActivity() {
private fun requestCallScreeningRoleIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val roleManager = getSystemService(Context.ROLE_SERVICE) as RoleManager
if (!roleManager.isRoleHeld(RoleManager.ROLE_CALL_SCREENING)) {
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_CALL_SCREENING)
roleLauncher.launch(intent)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Ask for overlay if needed
requestOverlay()
// Ask for call screening role if needed
requestCallScreeningRoleIfNeeded()
}
private fun simulateVipCall() {
val vipNumber = "+60189884118"
......
// OverlayPermissionModule.kt
package com.whoscall
import android.content.Intent
import android.net.Uri
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import android.os.Build
import android.provider.Settings
import com.facebook.react.bridge.*
class OverlayPermissionModule(private val reactContext: ReactApplicationContext)
: ReactContextBaseJavaModule(reactContext) {
class OverlayPermissionModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
override fun getName(): String = "OverlayPermission"
override fun getName(): String {
return "OverlayPermission"
}
@ReactMethod
fun hasOverlayPermission(promise: Promise) {
try {
val allowed = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Settings.canDrawOverlays(reactContext)
} else true
promise.resolve(allowed)
} catch (e: Exception) {
promise.reject("OVERLAY_CHECK_ERROR", e)
}
fun hasPermission(promise: Promise) {
val canDraw = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Settings.canDrawOverlays(reactApplicationContext)
} else {
true
}
@ReactMethod
fun openOverlaySettings() {
try {
val ctx = reactContext.currentActivity ?: reactContext
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${reactContext.packageName}")
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ctx.startActivity(intent)
} catch (_: Exception) {
// best-effort
promise.resolve(canDraw)
}
}
}
......@@ -366,6 +366,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = SLK4N5KC2N;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = whoscall/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
......@@ -379,7 +380,7 @@
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_BUNDLE_IDENTIFIER = com.whoscall;
PRODUCT_NAME = whoscall;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
......@@ -394,6 +395,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = SLK4N5KC2N;
INFOPLIST_FILE = whoscall/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
......@@ -406,7 +408,7 @@
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_BUNDLE_IDENTIFIER = com.whoscall;
PRODUCT_NAME = whoscall;
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
......@@ -427,6 +429,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SLK4N5KC2N;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
......@@ -443,7 +446,7 @@
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.reactjs.native.example.whoscall.whoscallDirectoryExtension;
PRODUCT_BUNDLE_IDENTIFIER = com.whoscall.whoscallDirectoryExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
......@@ -469,6 +472,7 @@
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SLK4N5KC2N;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
......@@ -484,7 +488,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.reactjs.native.example.whoscall.whoscallDirectoryExtension;
PRODUCT_BUNDLE_IDENTIFIER = com.whoscall.whoscallDirectoryExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = wholemodule;
......
#if os(iOS)
#if os(iOS)
import Foundation
import CallKit
......@@ -19,10 +20,7 @@ class CallDirectoryHandler: CXCallDirectoryProvider {
// Example: add VIP numbers
// Numbers must be E.164 format (+60 for Malaysia)
let vipNumbers: [(number: Int64, label: String)] = [
(60189884118, "Wei Yi"),
(60123456789, "Spam Caller"),
(60122553372, "Someone Else"),
(60123456789, "Scam Caller"),
(60189884118, "Wei Han"),
]
for entry in vipNumbers {
......@@ -50,3 +48,4 @@ extension CallDirectoryHandler: CXCallDirectoryExtensionContextDelegate {
}
}
#endif
#endif
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment