added fetch contact from real device icon

This commit is contained in:
Jaanu423
2025-09-19 12:26:57 +05:30
parent d4b638ce2f
commit 3b1dc58405
10 changed files with 382 additions and 26 deletions
+2
View File
@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<application
android:name=".MainApplication"
@@ -0,0 +1,48 @@
package com.circlecollect
import android.Manifest
import android.content.ContentResolver
import android.content.pm.PackageManager
import android.provider.ContactsContract
import androidx.core.app.ActivityCompat
import com.facebook.react.bridge.*
class ContactsModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
override fun getName(): String {
return "ContactsModule"
}
@ReactMethod
fun getContacts(promise: Promise) {
val context = reactApplicationContext
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
promise.reject("PERMISSION_DENIED", "Read contacts permission not granted")
return
}
val contactsList = Arguments.createArray()
val resolver: ContentResolver = context.contentResolver
val cursor = resolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
null,
null,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC"
)
cursor?.use {
val nameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
val numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
while (cursor.moveToNext()) {
val contact = Arguments.createMap()
contact.putString("name", cursor.getString(nameIndex))
contact.putString("phone", cursor.getString(numberIndex))
contactsList.pushMap(contact)
}
}
promise.resolve(contactsList)
}
}
@@ -0,0 +1,16 @@
package com.circlecollect
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class ContactsPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(ContactsModule(reactContext))
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
}
@@ -9,6 +9,7 @@ import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.circlecollect.ContactsPackage
class MainApplication : Application(), ReactApplication {
@@ -18,6 +19,7 @@ class MainApplication : Application(), ReactApplication {
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(ContactsPackage())
}
override fun getJSMainModuleName(): String = "index"
+7 -7
View File
@@ -23,7 +23,7 @@ export const RequestCard: FC<RequestCardProps> = ({ item, onApprove, onDecline }
/>
<View style={styles.userInfo}>
<Text style={styles.userName} numberOfLines={1}>{item.userName}</Text>
<Text style={styles.userMeta}><Ionicons name="call-outline" size={14} color={colors.textSecondary} /> {item.mobile}</Text>
<Text style={styles.userMeta}><Ionicons name="call-outline" size={14} color={colors.textLight} /> {item.mobile}</Text>
</View>
</View>
@@ -37,13 +37,13 @@ export const RequestCard: FC<RequestCardProps> = ({ item, onApprove, onDecline }
{/* Bottom section with action buttons */}
<View style={styles.actionsSection}>
<TouchableOpacity style={[styles.actionButton, styles.declineButton]} onPress={() => onDecline(item)}>
<Ionicons name="close-outline" size={20} color={colors.danger} />
<Text style={[styles.actionButtonText, { color: colors.danger }]}>Decline</Text>
<Ionicons name="close-outline" size={20} color={colors.pastelOrange} />
<Text style={[styles.actionButtonText, { color: colors.pastelOrange }]}>Decline</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, styles.approveButton]} onPress={() => onApprove(item)}>
{/* Corrected icon color and updated text color for better contrast */}
<Ionicons name="checkmark-outline" size={20} color={colors.textPrimary} />
<Text style={[styles.actionButtonText, { color: colors.textPrimary }]}>Approve</Text>
<Ionicons name="checkmark-outline" size={20} color={colors.textDark} />
<Text style={[styles.actionButtonText, { color: colors.textDark }]}>Approve</Text>
</TouchableOpacity>
</View>
</View>
@@ -77,7 +77,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: colors.border,
borderBottomColor: colors.textLight,
paddingBottom: 12,
},
avatar: {
@@ -125,7 +125,7 @@ const styles = StyleSheet.create({
borderRadius: 12,
},
declineButton: {
backgroundColor: colors.lightDanger,
backgroundColor: colors.pastelPink,
},
approveButton: {
// Set background color as requested
+29
View File
@@ -0,0 +1,29 @@
import { PermissionsAndroid, Platform, NativeModules } from "react-native";
const { ContactsModule } = NativeModules;
async function fetchContacts() {
if (Platform.OS === "android") {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.READ_CONTACTS,
{
title: "Contacts Permission",
message: "This app would like to view your contacts.",
buttonPositive: "OK",
}
);
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
console.log("Contacts permission denied");
return;
}
}
try {
const contacts = await ContactsModule.getContacts();
console.log("Contacts:", contacts);
} catch (error) {
console.error("Error fetching contacts:", error);
}
}
export { fetchContacts };
+3 -1
View File
@@ -12,6 +12,7 @@ import GroupReview from "../screens/GroupReview";
import BottomTabs from "./BottomTabs";
import BecomeOrg from "../screens/profile/BecomeOrg";
import EditGroupScreen from "../screens/management/EditGroupScreen";
import YourContacts from "../screens/management/YourContacts";
const Stack = createNativeStackNavigator();
export type RootStackParamList = {
@@ -62,7 +63,8 @@ export default function AppNavigator() {
<Stack.Screen name="GroupCreation" component={GroupCreation} options={{ headerShown: false }} />
<Stack.Screen name="GroupReview" component={GroupReview} options={{ headerShown: false }} />
<Stack.Screen name="BecomeOrg" component={BecomeOrg} options={{ headerShown: false }} />
<Stack.Screen name="EditGroup" component={EditGroupScreen} options={{ headerShown: false }} />
<Stack.Screen name="YourContacts" component={YourContacts} options={{ headerShown: false }} />
{/* <Stack.Screen name="EditGroup" component={EditGroupScreen} options={{ headerShown: false }} /> */}
</Stack.Navigator>
</NavigationContainer>
);
+35 -8
View File
@@ -23,6 +23,7 @@ import { MyGroup, JoinedGroup, JoinRequest, ApiResponse } from '../../types';
import { MyGroupCard } from "../../components/MyGroupCard";
import { JoinedGroupCard } from "../../components/JoinedGroupCard";
import { RequestCard } from "../../components/RequestCard";
import Ionicons from "@react-native-vector-icons/ionicons";
type GroupManagementNavProp = StackNavigationProp<RootStackParamList>;
type ActiveTab = "My Groups" | "Joined Groups" | "Requests";
@@ -44,7 +45,7 @@ const ManagementScreen: FC = () => {
try {
if (userRole === "ORGANIZER") {
// Fetch all data concurrently
const [myGroupsRes,joinedGroupsRes,requestsRes] = await Promise.all([
const [myGroupsRes, joinedGroupsRes, requestsRes] = await Promise.all([
apiCall(MY_GROUPS_URL, "GET") as Promise<ApiResponse<MyGroup[]>>,
apiCall(DECLINE_REQUEST_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>, // Assuming this is also needed
apiCall(REQUESTS_URL, "GET") as Promise<ApiResponse<JoinRequest[]>>,
@@ -74,7 +75,7 @@ const ManagementScreen: FC = () => {
const handleAction = async (url: string, body: object, successMessage: string, failureMessage: string, action: string) => {
// ... (logic remains the same)
console.log(url)
try {
try {
setLoading(true); // Optionally show a loading state during the action
// Make the POST request with an empty body
const res = await apiCall(url, "PUT", {}) as ApiResponse<null> | null;
@@ -93,15 +94,15 @@ const ManagementScreen: FC = () => {
}
};
const handleApprove = (req: JoinRequest) => handleAction(APPROVE_REQUEST_URL+req.requestId+"/approve", { requestId: req.requestId }, `${req.userName} has been added.`, "Failed to approve request.","Approve");
const handleDecline = (req: JoinRequest) => handleAction(DECLINE_REQUEST_URL+req.requestId+"/reject?reason=/"+" ", { requestId: req.requestId }, `Request from ${req.userName} has been declined.`, "Failed to decline request.","Decline");
const handleApprove = (req: JoinRequest) => handleAction(APPROVE_REQUEST_URL + req.requestId + "/approve", { requestId: req.requestId }, `${req.userName} has been added.`, "Failed to approve request.", "Approve");
const handleDecline = (req: JoinRequest) => handleAction(DECLINE_REQUEST_URL + req.requestId + "/reject?reason=/" + " ", { requestId: req.requestId }, `Request from ${req.userName} has been declined.`, "Failed to decline request.", "Decline");
const handleManageGroup = (group: MyGroup) => navigation.navigate('EditGroup', { groupId: group.id });
const renderContent = () => {
if (error) {
return <Text style={styles.emptyText}>{error}</Text>;
}
switch (activeTab) {
case "My Groups":
if (loading) {
@@ -151,9 +152,9 @@ const ManagementScreen: FC = () => {
default: return null;
}
};
const tabs: ActiveTab[] = userRole === 'ORGANIZER' ? ["My Groups", "Joined Groups", "Requests"] : ["Joined Groups"];
return (
<AppView style={styles.container} paddingHorizontal={0}>
<View style={styles.header}>
@@ -167,6 +168,32 @@ const ManagementScreen: FC = () => {
))}
</View>
<View style={styles.contentArea}>
{
activeTab === "Requests" && userRole === "ORGANIZER" ? (
<TouchableOpacity
style={{
position: "absolute",
bottom: 85,
right: 20,
backgroundColor: colors.pastelOrange,
width: 60,
height: 60,
borderRadius: 30,
justifyContent: "center",
alignItems: "center",
shadowColor: "#000",
shadowOpacity: 0.2,
shadowRadius: 5,
shadowOffset: { width: 0, height: 3 },
elevation: 5, // for Android shadow
}}
onPress={() => navigation.navigate("YourContacts" as never)}
>
<Ionicons name="person-add" size={28} color={colors.textDark} />
</TouchableOpacity>
) : null
}
{renderContent()}
</View>
</AppView>
@@ -176,7 +203,7 @@ const ManagementScreen: FC = () => {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f6fa'
backgroundColor: '#f5f6fa'
},
header: {
paddingHorizontal: 20,
+229
View File
@@ -0,0 +1,229 @@
import React, { useState, useEffect } from "react";
import {
StyleSheet,
Text,
View,
FlatList,
TouchableOpacity,
Modal,
PermissionsAndroid,
Platform,
NativeModules,
} from "react-native";
const { ContactsModule } = NativeModules;
const YourContacts = () => {
const [contacts, setContacts] = useState<any[]>([]);
const [selectedContact, setSelectedContact] = useState<any>(null);
const [modalVisible, setModalVisible] = useState(false);
useEffect(() => {
handleFetchContacts();
}, []);
const handleFetchContacts = async () => {
try {
if (Platform.OS === "android") {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.READ_CONTACTS,
{
title: "Contacts Permission",
message: "This app would like to view your contacts.",
buttonPositive: "OK",
}
);
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
console.log("Contacts permission denied");
return;
}
}
const fetchedContacts = await ContactsModule.getContacts();
setContacts(fetchedContacts);
} catch (error) {
console.error("Error fetching contacts:", error);
}
};
const handleSend = (contact: any) => {
setSelectedContact(contact);
setModalVisible(true);
};
const renderContact = ({ item }: { item: any }) => (
<View style={styles.contactRow}>
<View style={styles.avatar}>
<Text style={styles.avatarText}>
{item.name ? item.name.charAt(0).toUpperCase() : "?"}
</Text>
</View>
<View style={styles.contactInfo}>
<Text style={styles.contactName}>{item.name}</Text>
<Text style={styles.contactPhone}>{item.phone}</Text>
</View>
<TouchableOpacity
style={styles.inviteButton}
onPress={() => handleSend(item)}
>
<Text style={styles.inviteText}>Invite</Text>
</TouchableOpacity>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.header}>Select Contact</Text>
<FlatList
data={contacts}
keyExtractor={(item, index) => index.toString()}
renderItem={renderContact}
/>
{/* Send Modal */}
<Modal
transparent={true}
visible={modalVisible}
animationType="slide"
onRequestClose={() => setModalVisible(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<Text style={styles.modalTitle}>Send Invite</Text>
{selectedContact && (
<Text style={styles.modalText}>
Send invite to {selectedContact.name} ({selectedContact.phone})?
</Text>
)}
<View style={styles.modalActions}>
<TouchableOpacity
style={styles.cancelButton}
onPress={() => setModalVisible(false)}
>
<Text style={styles.cancelText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.sendButton}
onPress={() => setModalVisible(false)}
>
<Text style={styles.sendText}>Send</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</View>
);
};
export default YourContacts;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
paddingHorizontal: 10,
paddingTop: 20,
},
header: {
fontSize: 20,
fontWeight: "bold",
marginBottom: 15,
},
contactRow: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
borderBottomWidth: 0.5,
borderBottomColor: "#ddd",
},
avatar: {
width: 45,
height: 45,
borderRadius: 25,
backgroundColor: "#25D366",
justifyContent: "center",
alignItems: "center",
marginRight: 12,
},
avatarText: {
color: "#fff",
fontSize: 18,
fontWeight: "bold",
},
contactInfo: {
flex: 1,
},
contactName: {
fontSize: 16,
fontWeight: "600",
},
contactPhone: {
fontSize: 13,
color: "gray",
},
inviteButton: {
paddingHorizontal: 14,
paddingVertical: 6,
borderRadius: 8,
borderWidth: 1,
borderColor: "#25D366",
},
inviteText: {
color: "#25D366",
fontWeight: "bold",
},
modalOverlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.5)",
justifyContent: "center",
alignItems: "center",
},
modalContent: {
width: "80%",
backgroundColor: "#fff",
borderRadius: 10,
padding: 20,
alignItems: "center",
},
modalTitle: {
fontSize: 18,
fontWeight: "bold",
marginBottom: 10,
},
modalText: {
fontSize: 15,
marginBottom: 20,
textAlign: "center",
},
modalActions: {
flexDirection: "row",
justifyContent: "space-between",
width: "100%",
},
cancelButton: {
flex: 1,
marginRight: 10,
paddingVertical: 10,
borderRadius: 8,
backgroundColor: "#ddd",
alignItems: "center",
},
cancelText: {
fontWeight: "bold",
color: "#333",
},
sendButton: {
flex: 1,
marginLeft: 10,
paddingVertical: 10,
borderRadius: 8,
backgroundColor: "#25D366",
alignItems: "center",
},
sendText: {
fontWeight: "bold",
color: "#fff",
},
});
+11 -10
View File
@@ -23,6 +23,7 @@ import { useFocusEffect, useNavigation } from "@react-navigation/native";
import { ApiResponse } from "../home/UserProfileStatus";
import { BASE_URL } from "../../utils/ApiUrls";
import AppView from "../../components/AppView";
import { useSelector } from "react-redux";
@@ -47,6 +48,7 @@ type UserProfile = {
const Profile = () => {
const navigation = useNavigation<any>();
const user1 = useSelector((state: any) => state.user);
const [user, setUser] = useState<UserProfile | null>(null);
const [fetching, setFetching] = useState(true);
const [editing, setEditing] = useState(false);
@@ -222,7 +224,7 @@ const Profile = () => {
const response = await fetch(`${BASE_URL}user/profile/status`, {
method: "GET",
headers: {
Accept: "/",
Accept: "*/*",
Authorization: `Bearer ${token}`,
},
});
@@ -232,6 +234,7 @@ const Profile = () => {
console.log("👤 Profile Status Response:", json);
if (json.status === "success") {
setStatusData(json.data);
console.log("statusData response :", json);
} else {
setError(json.message || "Failed to fetch status");
@@ -273,6 +276,8 @@ const Profile = () => {
);
};
console.log("statusData?.role :", statusData?.role);
console.log("statusData?.organizerStatus :", statusData?.organizerStatus);
useFocusEffect(
useCallback(() => {
@@ -327,15 +332,11 @@ const Profile = () => {
{/* Profile Card */}
<View style={styles.profileCard}>
{/* Profile Image Section */}
{user?.role !== "USER"
&&
(user?.role === "ORGANIZER" &&
(user?.organizerStatus === "NOT_APPLIED" || user?.organizerStatus !== "PENDING")) ? (
<Text style={{ color: "#64748b", marginBottom: 16, textAlign: 'center', fontSize: 18 }}>Your Role: {user.role} | Organizer Status: {user.organizerStatus}</Text>
) : (
<SwitchNavigator role={user?.role || ''} status={user?.organizerStatus === "NOT_APPLIED" ? '' : user?.organizerStatus || ''} />
)}
{
statusData?.role && statusData?.organizerStatus ?
<Text style={{ color: "#64748b", marginBottom: 16, textAlign: 'center', fontSize: 18 }}>Your Role: {statusData.role} | Organizer Status: {statusData.organizerStatus}</Text>
:
<SwitchNavigator role={statusData?.role || ''} status={statusData?.organizerStatus || ''} />}
<View style={styles.profileImageSection}>
<View style={styles.avatarContainer}>
<Image