diff --git a/android/app/build.gradle b/android/app/build.gradle index 0380ba4..edf2433 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -117,3 +117,5 @@ dependencies { implementation jscFlavor } } + +apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle") \ No newline at end of file diff --git a/app/api/index.ts b/app/api/index.ts index 6a0ecc4..b09868c 100644 --- a/app/api/index.ts +++ b/app/api/index.ts @@ -1,11 +1,12 @@ -export * from './authApi'; -export * from './deliveryApi'; -export * from './onboardApi'; -export * from './productApi'; -export * from './cartApi'; -export * from './paymentMethodsApi'; -export * from './customerDetailsApi'; -export * from './orderApi'; -export * from './offerApi'; -export * from './reviewApi'; -export * from './walletApi'; \ No newline at end of file +export * from './authApi'; +export * from './deliveryApi'; +export * from './onboardApi'; +export * from './productApi'; +export * from './cartApi'; +export * from './paymentMethodsApi'; +export * from './customerDetailsApi'; +export * from './orderApi'; +export * from './offerApi'; +export * from './reviewApi'; +export * from './walletApi'; +export * from './supportApi'; \ No newline at end of file diff --git a/app/api/supportApi.ts b/app/api/supportApi.ts new file mode 100644 index 0000000..6625087 --- /dev/null +++ b/app/api/supportApi.ts @@ -0,0 +1,48 @@ +import { apiClient } from '@services'; +import { + SupportTicket, + CreateTicketPayload, + PostMessagePayload, + SupportMessage, +} from '@interfaces'; + +export const createTicketApi = async (payload: CreateTicketPayload) => { + return await apiClient.post('/support/tickets', payload); +}; + +export const getMyTicketsApi = async () => { + return await apiClient.get('/support/tickets'); +}; + +export const getTicketDetailsApi = async (ticketId: string) => { + return await apiClient.get(`/support/tickets/${ticketId}`); +}; + +export const postMessageApi = async ( + ticketId: string, + payload: PostMessagePayload, +) => { + return await apiClient.post( + `/support/tickets/${ticketId}/messages`, + payload, + ); +}; + +export const reopenTicketApi = async (ticketId: string) => { + return await apiClient.patch( + `/support/tickets/${ticketId}/reopen`, + {}, + ); +}; + +export const uploadSupportAttachmentApi = async (formData: FormData) => { + return await apiClient.post<{ id: string; filename: string; url: string }>( + '/support/upload', + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ); +}; diff --git a/app/features/screens/accountScreen/accountScreen.tsx b/app/features/screens/accountScreen/accountScreen.tsx index a34557b..a95ae4f 100644 --- a/app/features/screens/accountScreen/accountScreen.tsx +++ b/app/features/screens/accountScreen/accountScreen.tsx @@ -37,12 +37,12 @@ const MENU_SECTIONS: MenuSectionData[] = [ { title: 'Account', items: [ - { - icon: '๐Ÿ“', - label: 'My Addresses', - subLabel: 'Manage delivery addresses', - tint: '#FDECEA', - }, + // { + // icon: '๐Ÿ“', + // label: 'My Addresses', + // subLabel: 'Manage delivery addresses', + // tint: '#FDECEA', + // }, { icon: '๐Ÿ’ณ', label: 'Wallet', @@ -136,12 +136,12 @@ export const AccountScreen: React.FC = () => { - Edit Profile - + */} {/* Quick stats */} diff --git a/app/features/screens/helpSupportScreen/helpSupportScreen.styles.ts b/app/features/screens/helpSupportScreen/helpSupportScreen.styles.ts index 9983ee6..6a7d365 100644 --- a/app/features/screens/helpSupportScreen/helpSupportScreen.styles.ts +++ b/app/features/screens/helpSupportScreen/helpSupportScreen.styles.ts @@ -59,4 +59,190 @@ export const getStyles = (colors: any) => StyleSheet.create({ fontWeight: typography.fontWeight.semibold, color: colors.text, }, + tabContainer: { + flexDirection: 'row', + backgroundColor: colors.surface, + borderRadius: 8, + padding: 4, + marginBottom: 16, + }, + tabButton: { + flex: 1, + paddingVertical: 10, + alignItems: 'center', + borderRadius: 6, + }, + activeTabButton: { + backgroundColor: colors.cardBg, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + elevation: 2, + }, + tabText: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.semibold, + color: colors.textSecondary, + }, + activeTabText: { + color: colors.primary, + }, + ticketCard: { + backgroundColor: colors.cardBg, + borderRadius: 12, + padding: 16, + marginBottom: 12, + borderWidth: 1, + borderColor: colors.border, + }, + ticketHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + ticketNumber: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.bold, + color: colors.text, + }, + ticketDate: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + }, + ticketSubjectText: { + fontSize: typography.fontSize.sm, + color: colors.text, + marginBottom: 8, + }, + ticketFooter: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + borderTopWidth: 1, + borderTopColor: colors.border, + paddingTop: 8, + marginTop: 4, + }, + categoryBadge: { + backgroundColor: colors.surface, + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 4, + }, + categoryText: { + fontSize: 11, + color: colors.textSecondary, + }, + statusBadge: { + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 12, + }, + statusText: { + fontSize: 11, + fontWeight: typography.fontWeight.bold, + }, + createTicketBtn: { + backgroundColor: colors.primary, + borderRadius: 12, + paddingVertical: 14, + alignItems: 'center', + marginTop: 12, + marginBottom: 24, + }, + createTicketBtnText: { + color: '#FFFFFF', + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.bold, + }, + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.5)', + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + modalContent: { + backgroundColor: colors.cardBg, + borderRadius: 16, + width: '100%', + maxHeight: '90%', + padding: 20, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + modalTitle: { + fontSize: typography.fontSize.lg, + fontWeight: typography.fontWeight.bold, + color: colors.text, + marginBottom: 16, + textAlign: 'center', + }, + label: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.semibold, + color: colors.text, + marginBottom: 6, + marginTop: 12, + }, + input: { + backgroundColor: colors.background, + borderWidth: 1, + borderColor: colors.border, + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 10, + fontSize: typography.fontSize.sm, + color: colors.text, + }, + textArea: { + minHeight: 80, + textAlignVertical: 'top', + }, + categorySelect: { + backgroundColor: colors.background, + borderWidth: 1, + borderColor: colors.border, + borderRadius: 8, + padding: 12, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + categorySelectText: { + fontSize: typography.fontSize.sm, + color: colors.text, + }, + modalActions: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 20, + }, + modalBtn: { + flex: 1, + paddingVertical: 12, + borderRadius: 8, + alignItems: 'center', + }, + cancelBtn: { + backgroundColor: colors.border, + marginRight: 10, + }, + submitBtn: { + backgroundColor: colors.primary, + marginLeft: 10, + }, + cancelBtnText: { + color: colors.text, + fontWeight: typography.fontWeight.bold, + }, + submitBtnText: { + color: '#FFFFFF', + fontWeight: typography.fontWeight.bold, + }, }); diff --git a/app/features/screens/helpSupportScreen/helpSupportScreen.tsx b/app/features/screens/helpSupportScreen/helpSupportScreen.tsx index 0a463e1..d718cf7 100644 --- a/app/features/screens/helpSupportScreen/helpSupportScreen.tsx +++ b/app/features/screens/helpSupportScreen/helpSupportScreen.tsx @@ -1,11 +1,24 @@ -import React from 'react'; -import { View, Text, ScrollView, TouchableOpacity } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + ScrollView, + TouchableOpacity, + FlatList, + Modal, + TextInput, + ActivityIndicator, + Alert, +} from 'react-native'; +import { useNavigation, useIsFocused } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; import { getStyles } from './helpSupportScreen.styles'; import { Header } from '@components'; import { useAppTheme } from '@theme'; import { AppStackParamList } from '../../../navigation/appStack'; +import { useCustomerSupport } from '../../../hooks/useCustomerSupport'; +import { TicketCategory, TicketPriority } from '@interfaces'; +import { formatDate } from '../../../utils/helper'; type HelpSupportNavProp = StackNavigationProp; @@ -16,39 +29,297 @@ const FAQS = [ { q: 'What payment methods are accepted?', a: 'UPI, Credit/Debit Card, Wallet, and Cash on Delivery.' }, ]; +const TICKET_CATEGORIES: { value: TicketCategory; label: string }[] = [ + { value: 'ORDER', label: 'Order' }, + { value: 'PAYMENT', label: 'Payment' }, + { value: 'PAYOUT', label: 'Payout' }, + { value: 'TECHNICAL_ISSUE', label: 'Technical Issue' }, + { value: 'ACCOUNT', label: 'Account' }, + { value: 'OTHER', label: 'Other' }, +]; + export const HelpSupportScreen: React.FC = () => { const { colors } = useAppTheme(); const styles = getStyles(colors); const navigation = useNavigation(); + const isFocused = useIsFocused(); + + // Tab State + const [activeTab, setActiveTab] = useState<'faq' | 'tickets'>('faq'); + + // Custom Support Hook + const { tickets, loading, fetchTickets, createTicket } = useCustomerSupport(); + + // Create Ticket Modal State + const [createModalVisible, setCreateModalVisible] = useState(false); + const [categorySelectVisible, setCategorySelectVisible] = useState(false); + + const [category, setCategory] = useState('ORDER'); + const [priority] = useState('MEDIUM'); + const [subject, setSubject] = useState(''); + const [description, setDescription] = useState(''); + + useEffect(() => { + if (isFocused && activeTab === 'tickets') { + fetchTickets(); + } + }, [isFocused, activeTab, fetchTickets]); + + const handleCreateTicketSubmit = async () => { + if (!subject.trim()) { + Alert.alert('Error', 'Please enter a subject.'); + return; + } + if (!description.trim()) { + Alert.alert('Error', 'Please enter ticket details.'); + return; + } + + try { + const newTicket = await createTicket({ + category, + priority, + subject: subject.trim(), + description: description.trim(), + }); + setCreateModalVisible(false); + setSubject(''); + setDescription(''); + Alert.alert('Success', 'Support Ticket created successfully!'); + // Navigate to chat + navigation.navigate('SupportChatScreen', { ticketId: newTicket.id }); + } catch (error) { + Alert.alert('Error', 'Failed to create support ticket. Please try again.'); + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'OPEN': + return { bg: '#E3F2FD', text: '#1E88E5' }; // Blue + case 'IN_PROGRESS': + return { bg: '#FFF3E0', text: '#FB8C00' }; // Amber + case 'RESOLVED': + return { bg: '#E8F5E9', text: '#43A047' }; // Green + case 'CLOSED': + return { bg: '#ECEFF1', text: '#546E7A' }; // Grey + default: + return { bg: '#F5F5F5', text: '#9E9E9E' }; + } + }; + + const renderFaqTab = () => ( + + Frequently Asked Questions + {FAQS.map((faq, index) => ( + + {faq.q} + {faq.a} + + ))} + + Contact Support + + ๐Ÿ“ž + + Call us + +91 1800 123 4567 + + + + โœ‰๏ธ + + Email us + support@sgdelivery.com + + + + ); + + const renderTicketsTab = () => ( + + {loading && tickets.length === 0 ? ( + + + Fetching tickets... + + ) : ( + item.id} + showsVerticalScrollIndicator={false} + ListEmptyComponent={ + + + No support tickets found + + + Need help with your account or order? Create a ticket below! + + + } + renderItem={({ item }) => { + const statusStyle = getStatusColor(item.status); + return ( + navigation.navigate('SupportChatScreen', { ticketId: item.id })} + > + + Ticket #{item.ticketNumber} + {formatDate(item.createdAt)} + + + {item.subject} + + + + {item.category.replace('_', ' ')} + + + + {item.status.replace('_', ' ')} + + + + + ); + }} + /> + )} + + setCreateModalVisible(true)} + activeOpacity={0.8} + > + + Create Support Ticket + + + ); return (
navigation.goBack()} /> - - Frequently Asked Questions - {FAQS.map((faq, index) => ( - - {faq.q} - {faq.a} - - ))} - Contact Support - - ๐Ÿ“ž - - Call us - +91 1800 123 4567 + + + setActiveTab('faq')} + > + FAQs + + setActiveTab('tickets')} + > + + My Support Tickets + + + + + + {activeTab === 'faq' ? renderFaqTab() : renderTicketsTab()} + + {/* Create Ticket Modal */} + setCreateModalVisible(false)} + > + + + Create Support Ticket + + + Category + setCategorySelectVisible(true)} + > + + {TICKET_CATEGORIES.find((c) => c.value === category)?.label || 'Select category'} + + โ–ผ + + + Subject + + + Description + + + + setCreateModalVisible(false)} + > + Cancel + + + Submit + + + - - - โœ‰๏ธ - - Email us - support@sgdelivery.com + + + + {/* Category Dropdown Modal */} + setCategorySelectVisible(false)} + > + + + + Select Category + + {TICKET_CATEGORIES.map((item) => ( + { + setCategory(item.value); + setCategorySelectVisible(false); + }} + > + {item.label} + + ))} + setCategorySelectVisible(false)} + > + Close + - - + + ); }; +export default HelpSupportScreen; diff --git a/app/features/screens/helpSupportScreen/index.ts b/app/features/screens/helpSupportScreen/index.ts index cad255b..9af1dd0 100644 --- a/app/features/screens/helpSupportScreen/index.ts +++ b/app/features/screens/helpSupportScreen/index.ts @@ -1 +1,2 @@ export * from './helpSupportScreen'; + diff --git a/app/features/screens/homeScreen/homeScreen.tsx b/app/features/screens/homeScreen/homeScreen.tsx index 242db19..3eb9af1 100644 --- a/app/features/screens/homeScreen/homeScreen.tsx +++ b/app/features/screens/homeScreen/homeScreen.tsx @@ -31,6 +31,7 @@ import { } from '@store'; import { getAllCategoriesThunk, getAllProductsThunk } from './thunk'; import { getCategoryEmoji } from '@utils'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; type NavProp = CompositeNavigationProp< BottomTabNavigationProp, @@ -121,7 +122,11 @@ export const HomeScreen: React.FC = () => { - ๐Ÿ“ + Deliver to @@ -139,7 +144,11 @@ export const HomeScreen: React.FC = () => { activeOpacity={0.7} onPress={() => navigation.navigate('CartScreen')} > - ๐Ÿงบ + diff --git a/app/features/screens/index.ts b/app/features/screens/index.ts index fe8b560..0d66c29 100644 --- a/app/features/screens/index.ts +++ b/app/features/screens/index.ts @@ -22,3 +22,4 @@ export * from './accountScreen'; export * from './writeReviewScreen'; export * from './orderDetailsScreen'; export * from './walletScreen'; +export * from './supportChatScreen' diff --git a/app/features/screens/orderDetailsScreen/orderDetailsScreen.styles.ts b/app/features/screens/orderDetailsScreen/orderDetailsScreen.styles.ts index 59a5d04..68619f6 100644 --- a/app/features/screens/orderDetailsScreen/orderDetailsScreen.styles.ts +++ b/app/features/screens/orderDetailsScreen/orderDetailsScreen.styles.ts @@ -252,4 +252,104 @@ export const getStyles = (colors: any) => color: '#92400E', fontWeight: typography.fontWeight.medium, }, + orderNeedHelpBtn: { + marginTop: 14, + paddingVertical: 10, + paddingHorizontal: 12, + backgroundColor: '#E8F5E9', + borderRadius: 8, + borderWidth: 1, + borderColor: '#C8E6C9', + alignItems: 'center', + justifyContent: 'center', + }, + orderNeedHelpText: { + color: colors.primary, + fontWeight: typography.fontWeight.bold, + fontSize: typography.fontSize.sm, + }, + // Help modal specific styles + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.5)', + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + modalContent: { + backgroundColor: colors.cardBg, + borderRadius: 16, + width: '100%', + padding: 20, + }, + modalTitle: { + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.bold, + color: colors.text, + marginBottom: 16, + textAlign: 'center', + }, + label: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.semibold, + color: colors.text, + marginBottom: 6, + marginTop: 12, + }, + input: { + backgroundColor: colors.background, + borderWidth: 1, + borderColor: colors.border, + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 10, + fontSize: typography.fontSize.sm, + color: colors.text, + }, + textArea: { + minHeight: 80, + textAlignVertical: 'top', + }, + categorySelect: { + backgroundColor: colors.background, + borderWidth: 1, + borderColor: colors.border, + borderRadius: 8, + padding: 12, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + categorySelectText: { + fontSize: typography.fontSize.sm, + color: colors.text, + }, + modalActions: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 20, + }, + modalBtn: { + flex: 1, + paddingVertical: 12, + borderRadius: 8, + alignItems: 'center', + }, + cancelBtn: { + backgroundColor: colors.border, + marginRight: 10, + }, + submitBtn: { + backgroundColor: colors.primary, + marginLeft: 10, + }, + cancelBtnText: { + color: colors.text, + fontWeight: typography.fontWeight.bold, + }, + submitBtnText: { + color: '#FFFFFF', + fontWeight: typography.fontWeight.bold, + }, }); + diff --git a/app/features/screens/orderDetailsScreen/orderDetailsScreen.tsx b/app/features/screens/orderDetailsScreen/orderDetailsScreen.tsx index 0b7b20b..d4efa2c 100644 --- a/app/features/screens/orderDetailsScreen/orderDetailsScreen.tsx +++ b/app/features/screens/orderDetailsScreen/orderDetailsScreen.tsx @@ -5,6 +5,9 @@ import { ScrollView, ActivityIndicator, TouchableOpacity, + Modal, + TextInput, + Alert, } from 'react-native'; import { useRoute, useNavigation } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; @@ -16,8 +19,9 @@ import { RootState, useAppDispatch, useAppSelector } from '@store'; import { getOrderByIdThunk } from '../checkoutPaymentScreen'; import { formatDate, formatTime } from '@utils/helper'; import { submitReview } from './thunk'; -import { giveRatingPayload, OrderItem } from '@interfaces'; +import { giveRatingPayload, OrderItem, TicketCategory } from '@interfaces'; import { AppStackParamList } from '@navigation'; +import { useCustomerSupport } from '../../../hooks/useCustomerSupport'; type OrderDetailsRouteProp = RouteProp; type OrderDetailsNavProp = StackNavigationProp; @@ -45,6 +49,43 @@ export const OrderDetailsScreen: React.FC = () => { const isDelivered = orderDetails?.status === 'DELIVERED'; + // โ”€โ”€ Support / Need Help modal state โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const [helpModalVisible, setHelpModalVisible] = useState(false); + const [categorySelectVisible, setCategorySelectVisible] = useState(false); + const [helpCategory, setHelpCategory] = useState('ORDER'); + const [helpDescription, setHelpDescription] = useState(''); + const [submittingTicket, setSubmittingTicket] = useState(false); + + const { createTicket } = useCustomerSupport(); + + const handleHelpSubmit = async () => { + if (!orderDetails) return; + if (!helpDescription.trim()) { + Alert.alert('Error', 'Please describe the issue in detail.'); + return; + } + + setSubmittingTicket(true); + try { + const orderShortId = orderDetails.orderNumber?.split('-').pop() || orderDetails.id; + const newTicket = await createTicket({ + category: helpCategory, + priority: 'MEDIUM', + subject: `Issue with Order #${orderShortId}`, + description: helpDescription.trim(), + orderId: orderDetails.id, + }); + setHelpModalVisible(false); + setHelpDescription(''); + Alert.alert('Success', 'Support ticket created successfully!'); + navigation.navigate('SupportChatScreen', { ticketId: newTicket.id }); + } catch (error) { + Alert.alert('Error', 'Failed to create support ticket. Please try again.'); + } finally { + setSubmittingTicket(false); + } + }; + useEffect(() => { if (orderId) { dispatch(getOrderByIdThunk(orderId)); @@ -140,6 +181,15 @@ export const OrderDetailsScreen: React.FC = () => { {orderDetails.merchant?.name || 'Store'} + + {/* Need Help Button */} + setHelpModalVisible(true)} + activeOpacity={0.7} + > + โ“ Need help with this order? + {/* Order Status Timeline Section */} @@ -289,6 +339,113 @@ export const OrderDetailsScreen: React.FC = () => { onSubmit={handleRatingSubmit} onClose={handleModalClose} /> + + {/* Need Help / Ticket Creation Modal */} + setHelpModalVisible(false)} + > + + + Report Issue with Order + + + Category + setCategorySelectVisible(true)} + > + + {helpCategory.replace('_', ' ')} + + โ–ผ + + + Order ID + + + Details + + + + setHelpModalVisible(false)} + disabled={submittingTicket} + > + Cancel + + + {submittingTicket ? ( + + ) : ( + Submit + )} + + + + + + + + {/* Help Category Select Modal */} + setCategorySelectVisible(false)} + > + + + + Select Issue Category + + {[ + { value: 'ORDER', label: 'Order' }, + { value: 'PAYMENT', label: 'Payment' }, + { value: 'PAYOUT', label: 'Payout' }, + { value: 'TECHNICAL_ISSUE', label: 'Technical Issue' }, + { value: 'ACCOUNT', label: 'Account' }, + { value: 'OTHER', label: 'Other' }, + ].map((item) => ( + { + setHelpCategory(item.value as TicketCategory); + setCategorySelectVisible(false); + }} + > + {item.label} + + ))} + setCategorySelectVisible(false)} + > + Close + + + + ); }; diff --git a/app/features/screens/providerDetailsScreen/providerDetailsScreen.tsx b/app/features/screens/providerDetailsScreen/providerDetailsScreen.tsx index f374032..d6097ba 100644 --- a/app/features/screens/providerDetailsScreen/providerDetailsScreen.tsx +++ b/app/features/screens/providerDetailsScreen/providerDetailsScreen.tsx @@ -112,9 +112,9 @@ export const ProviderDetailsScreen: React.FC = () => { dispatch(updateCartItemThunk({ productId: product.id, quantity: newQty })); }; - const handleRateProduct = () => { - navigation.navigate('WriteReviewScreen', { productId: product?.id ?? '' }); - }; + // const handleRateProduct = () => { + // navigation.navigate('WriteReviewScreen', { productId: product?.id ?? '' }); + // }; const renderStars = (value: number, size = 14) => { const rounded = Math.round(value); @@ -150,15 +150,15 @@ export const ProviderDetailsScreen: React.FC = () => { const images = product?.media?.length ? product.media : [ - { - id: 'fallback', - url: product?.imageUrl || '', - mediaType: 'IMAGE', - sortOrder: 0, - productId: '', - createdAt: '', - }, - ]; + { + id: 'fallback', + url: product?.imageUrl || '', + mediaType: 'IMAGE', + sortOrder: 0, + productId: '', + createdAt: '', + }, + ]; return ( @@ -310,7 +310,7 @@ export const ProviderDetailsScreen: React.FC = () => { โญ No ratings yet - + {/* Be the first to share what you think of this product. { onPress={handleRateProduct} > Rate this product - + */} )} @@ -342,9 +342,9 @@ export const ProviderDetailsScreen: React.FC = () => { {review.createdAt ? new Date(review.createdAt).toLocaleDateString('en-IN', { - day: 'numeric', - month: 'short', - }) + day: 'numeric', + month: 'short', + }) : ''} @@ -500,8 +500,8 @@ export const ProviderDetailsScreen: React.FC = () => { {!product ? 'Loading...' : product.isTrackStock && product.stockQuantity === 0 - ? 'Out of Stock' - : 'Add to Cart'} + ? 'Out of Stock' + : 'Add to Cart'} diff --git a/app/features/screens/supportChatScreen/index.ts b/app/features/screens/supportChatScreen/index.ts new file mode 100644 index 0000000..86ecd4c --- /dev/null +++ b/app/features/screens/supportChatScreen/index.ts @@ -0,0 +1 @@ +export * from './supportChatScreen'; \ No newline at end of file diff --git a/app/features/screens/supportChatScreen/supportChatScreen.styles.ts b/app/features/screens/supportChatScreen/supportChatScreen.styles.ts new file mode 100644 index 0000000..a7ec29a --- /dev/null +++ b/app/features/screens/supportChatScreen/supportChatScreen.styles.ts @@ -0,0 +1,236 @@ +import { StyleSheet } from 'react-native'; +import { typography } from '@theme'; + +export const getStyles = (colors: any) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + headerRight: { + marginRight: 16, + }, + statusBadge: { + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + }, + statusText: { + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.bold, + }, + ticketInfoBar: { + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: colors.cardBg, + borderBottomWidth: 1, + borderBottomColor: colors.border, + }, + ticketSubject: { + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.bold, + color: colors.text, + marginBottom: 4, + }, + ticketMeta: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + }, + chatList: { + padding: 16, + paddingBottom: 24, + }, + messageRow: { + flexDirection: 'row', + marginBottom: 16, + width: '100%', + }, + customerRow: { + justifyContent: 'flex-end', + }, + agentRow: { + justifyContent: 'flex-start', + }, + bubble: { + maxWidth: '75%', + borderRadius: 16, + paddingHorizontal: 14, + paddingVertical: 10, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 2, + elevation: 1, + }, + customerBubble: { + backgroundColor: colors.primary, + borderBottomRightRadius: 2, + }, + agentBubble: { + backgroundColor: colors.cardBg, + borderBottomLeftRadius: 2, + borderWidth: 1, + borderColor: colors.border, + }, + senderName: { + fontSize: 11, + fontWeight: typography.fontWeight.semibold, + color: colors.textSecondary, + marginBottom: 4, + }, + messageText: { + fontSize: typography.fontSize.sm, + lineHeight: 20, + }, + customerText: { + color: '#FFFFFF', + }, + agentText: { + color: colors.text, + }, + timeText: { + fontSize: 9, + marginTop: 4, + textAlign: 'right', + }, + customerTime: { + color: 'rgba(255,255,255,0.7)', + }, + agentTime: { + color: colors.textSecondary, + }, + attachmentImage: { + width: 200, + height: 150, + borderRadius: 8, + marginBottom: 4, + }, + typingContainer: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: colors.background, + }, + typingText: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + fontStyle: 'italic', + marginLeft: 6, + }, + inputContainer: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + paddingVertical: 10, + backgroundColor: colors.cardBg, + borderTopWidth: 1, + borderTopColor: colors.border, + }, + attachmentBtn: { + padding: 8, + marginRight: 4, + justifyContent: 'center', + alignItems: 'center', + }, + input: { + flex: 1, + minHeight: 40, + maxHeight: 100, + backgroundColor: colors.background, + borderWidth: 1, + borderColor: colors.border, + borderRadius: 20, + paddingHorizontal: 16, + paddingVertical: 8, + fontSize: typography.fontSize.sm, + color: colors.text, + marginRight: 8, + }, + sendBtn: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: colors.primary, + justifyContent: 'center', + alignItems: 'center', + }, + sendBtnDisabled: { + backgroundColor: colors.border, + }, + sendIcon: { + fontSize: 16, + color: '#FFFFFF', + }, + previewContainer: { + flexDirection: 'row', + padding: 12, + backgroundColor: colors.cardBg, + borderTopWidth: 1, + borderTopColor: colors.border, + }, + previewImageWrapper: { + position: 'relative', + marginRight: 12, + }, + previewImage: { + width: 80, + height: 80, + borderRadius: 8, + }, + removePreviewBtn: { + position: 'absolute', + top: -6, + right: -6, + backgroundColor: '#FF3B30', + width: 20, + height: 20, + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 1, + borderColor: '#FFFFFF', + }, + removePreviewText: { + color: '#FFFFFF', + fontSize: 10, + fontWeight: 'bold', + }, + reopenContainer: { + padding: 16, + backgroundColor: colors.cardBg, + borderTopWidth: 1, + borderTopColor: colors.border, + alignItems: 'center', + }, + reopenTitle: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.semibold, + color: colors.textSecondary, + marginBottom: 10, + textAlign: 'center', + }, + reopenBtn: { + paddingHorizontal: 24, + paddingVertical: 12, + backgroundColor: colors.primary, + borderRadius: 24, + width: '100%', + alignItems: 'center', + }, + reopenBtnText: { + color: '#FFFFFF', + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.bold, + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + loadingText: { + marginTop: 12, + fontSize: typography.fontSize.sm, + color: colors.textSecondary, + }, + }); diff --git a/app/features/screens/supportChatScreen/supportChatScreen.tsx b/app/features/screens/supportChatScreen/supportChatScreen.tsx new file mode 100644 index 0000000..c8264f4 --- /dev/null +++ b/app/features/screens/supportChatScreen/supportChatScreen.tsx @@ -0,0 +1,373 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + FlatList, + Image, + ActivityIndicator, + KeyboardAvoidingView, + Platform, + Alert, + Modal, +} from 'react-native'; +import { useRoute, useNavigation, RouteProp } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { Header } from '@components'; +import { useAppTheme } from '@theme'; +import { useCustomerSupport } from '@hooks'; +import { getStyles } from './supportChatScreen.styles'; +import { AppStackParamList } from '../../../navigation/appStack'; +import { formatTime, getFullUrl } from '../../../utils/helper'; + +type SupportChatRouteProp = RouteProp; +type SupportChatNavProp = StackNavigationProp; + +// Mock attachments that users can select to simulate image attachments +const MOCK_ATTACHMENTS = [ + { + id: 'att1', + label: 'Receipt Invoice', + url: 'https://images.unsplash.com/photo-1554415707-6e8cfc93fe23?w=500&auto=format&fit=crop&q=60', + }, + { + id: 'att2', + label: 'Damaged Packing', + url: 'https://images.unsplash.com/photo-1607344645866-009c320c5ab8?w=500&auto=format&fit=crop&q=60', + }, + { + id: 'att3', + label: 'Wrong Item Delivered', + url: 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=500&auto=format&fit=crop&q=60', + }, +]; + +export const SupportChatScreen: React.FC = () => { + const { colors } = useAppTheme(); + const styles = getStyles(colors); + const route = useRoute(); + const navigation = useNavigation(); + const { ticketId } = route.params; + + const { + currentTicket, + messages, + loading, + isAgentTyping, + fetchTicketDetails, + sendMessage, + reopenTicket, + emitTypingStart, + emitTypingStop, + } = useCustomerSupport(ticketId); + + const [inputText, setInputText] = useState(''); + const [selectedAttachmentUrl, setSelectedAttachmentUrl] = useState(null); + const [attachmentModalVisible, setAttachmentModalVisible] = useState(false); + const flatListRef = useRef(null); + const typingTimeoutRef = useRef(null); + + useEffect(() => { + if (ticketId) { + fetchTicketDetails(ticketId); + } + }, [ticketId, fetchTicketDetails]); + + // Auto scroll to bottom when messages change + useEffect(() => { + if (messages.length > 0) { + setTimeout(() => { + flatListRef.current?.scrollToEnd({ animated: true }); + }, 100); + } + }, [messages]); + + const handleSend = async () => { + if (!inputText.trim() && !selectedAttachmentUrl) return; + + try { + const textToSend = inputText.trim(); + const attachmentsToSend = selectedAttachmentUrl ? [selectedAttachmentUrl] : undefined; + + setInputText(''); + setSelectedAttachmentUrl(null); + emitTypingStop(); + + await sendMessage(ticketId, textToSend, attachmentsToSend); + } catch (error) { + Alert.alert('Error', 'Failed to send message. Please try again.'); + } + }; + + const handleInputChange = (text: string) => { + setInputText(text); + + // Emit typing start and manage typing stop timeout + emitTypingStart(); + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current); + } + typingTimeoutRef.current = setTimeout(() => { + emitTypingStop(); + }, 2000); + }; + + const handleReopen = async () => { + try { + await reopenTicket(ticketId); + Alert.alert('Success', 'Ticket has been reopened.'); + } catch (error) { + Alert.alert('Error', 'Failed to reopen ticket.'); + } + }; + + const getStatusColor = (status?: string) => { + switch (status) { + case 'OPEN': + return { bg: '#E3F2FD', text: '#1E88E5' }; // Blue + case 'IN_PROGRESS': + return { bg: '#FFF3E0', text: '#FB8C00' }; // Amber + case 'RESOLVED': + return { bg: '#E8F5E9', text: '#43A047' }; // Green + case 'CLOSED': + return { bg: '#ECEFF1', text: '#546E7A' }; // Grey + default: + return { bg: '#F5F5F5', text: '#9E9E9E' }; + } + }; + + const statusStyle = getStatusColor(currentTicket?.status); + + if (loading && !currentTicket) { + return ( + +
navigation.goBack()} /> + + + Loading support ticket... + + + ); + } + + const isClosed = currentTicket?.status === 'RESOLVED' || currentTicket?.status === 'CLOSED'; + + return ( + +
navigation.goBack()} + rightComponent={ + currentTicket && ( + + + {currentTicket.status.replace('_', ' ')} + + + ) + } + /> + + {currentTicket && ( + + {currentTicket.subject} + + ID: #{currentTicket.ticketNumber} | Category: {currentTicket.category.replace('_', ' ')} + + + )} + + item.id} + contentContainerStyle={styles.chatList} + renderItem={({ item }) => { + const isCustomer = item.sender.roleEnum === 'CUSTOMER'; + return ( + + + {!isCustomer && ( + {item.sender.name} (Support) + )} + {item.attachments && item.attachments.map((att: string, idx: number) => ( + + ))} + {item.message.trim() ? ( + + {item.message} + + ) : null} + + {formatTime(item.createdAt)} + + + + ); + }} + onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: true })} + /> + + {isAgentTyping && ( + + + Agent is typing... + + )} + + {/* Selected attachment preview */} + {selectedAttachmentUrl && ( + + + + setSelectedAttachmentUrl(null)} + > + โœ• + + + + )} + + {/* Input or Reopen prompt */} + {isClosed ? ( + + + This ticket has been resolved or closed. + + + Reopen Ticket + + + ) : ( + + setAttachmentModalVisible(true)} + > + ๐Ÿ“Ž + + + + โžค + + + )} + + {/* Attachment Modal */} + setAttachmentModalVisible(false)} + > + + + + Select Mock Attachment + + {MOCK_ATTACHMENTS.map((item) => ( + { + setSelectedAttachmentUrl(item.url); + setAttachmentModalVisible(false); + }} + > + ๐Ÿ–ผ๏ธ + {item.label} + + ))} + setAttachmentModalVisible(false)} + > + Cancel + + + + + + ); +}; +export default SupportChatScreen; diff --git a/app/hooks/index.ts b/app/hooks/index.ts index e69de29..c168a0f 100644 --- a/app/hooks/index.ts +++ b/app/hooks/index.ts @@ -0,0 +1 @@ +export * from './useCustomerSupport'; diff --git a/app/hooks/useCustomerSupport.ts b/app/hooks/useCustomerSupport.ts new file mode 100644 index 0000000..af59ddd --- /dev/null +++ b/app/hooks/useCustomerSupport.ts @@ -0,0 +1,246 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import { io, Socket } from 'socket.io-client'; +import { tokenManager } from '@services'; +import { + SupportTicket, + SupportMessage, + CreateTicketPayload, +} from '@interfaces'; +import { + createTicketApi, + getMyTicketsApi, + getTicketDetailsApi, + postMessageApi, + reopenTicketApi, + uploadSupportAttachmentApi, +} from '../api/supportApi'; + +const BASE_URL = 'https://accb-115-187-33-94.ngrok-free.app'; +const SUPPORT_SOCKET_URL = `${BASE_URL}/support`; + +export const useCustomerSupport = (ticketId?: string) => { + const [tickets, setTickets] = useState([]); + const [currentTicket, setCurrentTicket] = useState(null); + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(false); + const [isAgentTyping, setIsAgentTyping] = useState(false); + const [socketConnected, setSocketConnected] = useState(false); + + const socketRef = useRef(null); + + // 1. Fetch all my tickets + const fetchTickets = useCallback(async () => { + setLoading(true); + try { + const data = await getMyTicketsApi(); + setTickets(data); + } catch (error) { + console.error('[useCustomerSupport] fetchTickets error:', error); + } finally { + setLoading(false); + } + }, []); + + // 2. Fetch single ticket details + const fetchTicketDetails = useCallback(async (id: string) => { + setLoading(true); + try { + const data = await getTicketDetailsApi(id); + setCurrentTicket(data); + setMessages(data.messages || []); + } catch (error) { + console.error('[useCustomerSupport] fetchTicketDetails error:', error); + } finally { + setLoading(false); + } + }, []); + + // 3. Create ticket + const createTicket = useCallback(async (payload: CreateTicketPayload) => { + setLoading(true); + try { + const newTicket = await createTicketApi(payload); + setTickets(prev => [newTicket, ...prev]); + return newTicket; + } catch (error) { + console.error('[useCustomerSupport] createTicket error:', error); + throw error; + } finally { + setLoading(false); + } + }, []); + + // 4. Send reply message + const sendMessage = useCallback(async (id: string, text: string, attachments?: string[]) => { + try { + const newMsg = await postMessageApi(id, { message: text, attachments }); + setMessages(prev => { + if (prev.some(m => m.id === newMsg.id)) return prev; + return [...prev, newMsg]; + }); + return newMsg; + } catch (error) { + console.error('[useCustomerSupport] sendMessage error:', error); + throw error; + } + }, []); + + // 5. Reopen ticket + const reopenTicket = useCallback(async (id: string) => { + setLoading(true); + try { + const updatedTicket = await reopenTicketApi(id); + setCurrentTicket(updatedTicket); + return updatedTicket; + } catch (error) { + console.error('[useCustomerSupport] reopenTicket error:', error); + throw error; + } finally { + setLoading(false); + } + }, []); + + // 6. Upload attachment + const uploadAttachment = useCallback(async (file: { uri: string; name: string; type: string }) => { + const formData = new FormData(); + formData.append('file', { + uri: file.uri, + name: file.name, + type: file.type, + } as any); + + try { + const response = await uploadSupportAttachmentApi(formData); + return response; + } catch (error) { + console.error('[useCustomerSupport] uploadAttachment error:', error); + throw error; + } + }, []); + + // 7. Client typing emits + const emitTypingStart = useCallback(() => { + if (socketRef.current && ticketId) { + socketRef.current.emit('typing_start', { ticketId }); + } + }, [ticketId]); + + const emitTypingStop = useCallback(() => { + if (socketRef.current && ticketId) { + socketRef.current.emit('typing_stop', { ticketId }); + } + }, [ticketId]); + + // 8. Socket Connection and Event Listeners Lifecycle + useEffect(() => { + if (!ticketId) { + if (socketRef.current) { + socketRef.current.disconnect(); + socketRef.current = null; + setSocketConnected(false); + } + return; + } + + let active = true; + + const setupSocket = async () => { + const token = await tokenManager.getAccessToken(); + if (!token || !active) return; + + const socket = io(SUPPORT_SOCKET_URL, { + auth: { token }, + transports: ['websocket'], + reconnection: true, + reconnectionAttempts: Infinity, + reconnectionDelay: 2000, + }); + + socketRef.current = socket; + + socket.on('connect', () => { + if (!active) return; + setSocketConnected(true); + console.log('[useCustomerSupport Socket] Connected to /support'); + socket.emit('join_ticket_room', { ticketId }); + }); + + socket.on('disconnect', (reason) => { + if (!active) return; + setSocketConnected(false); + console.log('[useCustomerSupport Socket] Disconnected:', reason); + }); + + socket.on('support:joined', (data) => { + console.log('[useCustomerSupport Socket] Joined room status:', data); + }); + + socket.on('support:new_message', (msg: SupportMessage) => { + if (!active) return; + if (msg.ticketId === ticketId) { + setMessages(prev => { + if (prev.some(m => m.id === msg.id)) return prev; + return [...prev, msg]; + }); + } + }); + + socket.on('support:status_changed', (data: { ticketId: string; status: any }) => { + if (!active) return; + if (data.ticketId === ticketId) { + setCurrentTicket(prev => prev ? { ...prev, status: data.status } : null); + } + }); + + socket.on('support:typing_start', (data: { ticketId: string; role: string }) => { + if (!active) return; + if (data.ticketId === ticketId && data.role === 'ADMIN') { + setIsAgentTyping(true); + } + }); + + socket.on('support:typing_stop', (data: { ticketId: string }) => { + if (!active) return; + if (data.ticketId === ticketId) { + setIsAgentTyping(false); + } + }); + }; + + setupSocket(); + + return () => { + active = false; + if (socketRef.current) { + socketRef.current.emit('leave_ticket_room', { ticketId }); + socketRef.current.off('connect'); + socketRef.current.off('disconnect'); + socketRef.current.off('support:joined'); + socketRef.current.off('support:new_message'); + socketRef.current.off('support:status_changed'); + socketRef.current.off('support:typing_start'); + socketRef.current.off('support:typing_stop'); + socketRef.current.disconnect(); + socketRef.current = null; + setSocketConnected(false); + } + }; + }, [ticketId]); + + return { + tickets, + currentTicket, + messages, + loading, + isAgentTyping, + socketConnected, + fetchTickets, + fetchTicketDetails, + createTicket, + sendMessage, + reopenTicket, + uploadAttachment, + emitTypingStart, + emitTypingStop, + }; +}; diff --git a/app/interfaces/index.ts b/app/interfaces/index.ts index f3ff92f..64d3446 100644 --- a/app/interfaces/index.ts +++ b/app/interfaces/index.ts @@ -99,3 +99,4 @@ export * from './order'; export * from './offers'; export * from './productRating'; export * from './wallet'; +export * from './support'; diff --git a/app/interfaces/support.ts b/app/interfaces/support.ts new file mode 100644 index 0000000..0c79459 --- /dev/null +++ b/app/interfaces/support.ts @@ -0,0 +1,51 @@ +export type TicketCategory = + | 'ORDER' + | 'PAYMENT' + | 'PAYOUT' + | 'TECHNICAL_ISSUE' + | 'ACCOUNT' + | 'OTHER'; + +export type TicketPriority = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; +export type TicketStatus = 'OPEN' | 'IN_PROGRESS' | 'RESOLVED' | 'CLOSED'; + +export interface SupportMessage { + id: string; + ticketId: string; + senderId: string; + message: string; + attachments?: string[]; + sender: { + name: string; + roleEnum: 'CUSTOMER' | 'DRIVER' | 'ADMIN'; + }; + createdAt: string; +} + +export interface SupportTicket { + id: string; + ticketNumber: string; + category: TicketCategory; + priority: TicketPriority; + status: TicketStatus; + subject: string; + description: string; + orderId?: string; + customerId: string; + createdAt: string; + updatedAt: string; + messages?: SupportMessage[]; +} + +export interface CreateTicketPayload { + category: TicketCategory; + priority: TicketPriority; + subject: string; + description: string; + orderId?: string; +} + +export interface PostMessagePayload { + message: string; + attachments?: string[]; +} diff --git a/app/navigation/appStack.tsx b/app/navigation/appStack.tsx index 8f09c70..5e67a09 100644 --- a/app/navigation/appStack.tsx +++ b/app/navigation/appStack.tsx @@ -15,6 +15,7 @@ import { HelpSupportScreen, WriteReviewScreen, OrderDetailsScreen, + SupportChatScreen, } from '@features/screens'; import WalletScreen from '@features/screens/walletScreen/walletScreen'; @@ -32,6 +33,7 @@ export type AppStackParamList = { HelpSupportScreen: undefined; WriteReviewScreen: { productId: string } | undefined; OrderDetailsScreen: { orderId: string } | undefined; + SupportChatScreen: { ticketId: string }; WalletScreen: undefined; }; @@ -71,6 +73,7 @@ export const AppStack: React.FC = () => { + ); diff --git a/app/navigation/mainTabNavigator.tsx b/app/navigation/mainTabNavigator.tsx index 81b06a0..638270f 100644 --- a/app/navigation/mainTabNavigator.tsx +++ b/app/navigation/mainTabNavigator.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import { Text } from 'react-native'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; + import { HomeScreen, SearchScreen, @@ -19,33 +20,58 @@ export type MainTabParamList = { const Tab = createBottomTabNavigator(); -const tabIcons: Record = { - HomeScreen: '๐Ÿ ', - SearchScreen: '๐Ÿ”', - MyOrdersScreen: '๐Ÿ“ฆ', - OffersScreen: '๐Ÿท๏ธ', - AccountScreen: '๐Ÿ‘ค', -}; - -const renderTabIcon = (routeName: string, focused: boolean) => ( - - {tabIcons[routeName]} - -); - export const MainTabNavigator: React.FC = () => { return ( ({ headerShown: false, - tabBarIcon: ({ focused }) => renderTabIcon(route.name, focused), + + tabBarIcon: ({ color, size, focused }) => { + let iconName: string; + + switch (route.name) { + case 'HomeScreen': + iconName = focused ? 'home' : 'home-outline'; + break; + + case 'SearchScreen': + iconName = 'magnify'; + break; + + case 'MyOrdersScreen': + iconName = focused ? 'package-variant' : 'package-variant-closed'; + break; + + case 'OffersScreen': + iconName = focused ? 'tag' : 'tag-outline'; + break; + + case 'AccountScreen': + iconName = focused ? 'account' : 'account-outline'; + break; + + default: + iconName = 'circle'; + } + + return ( + + ); + }, + tabBarActiveTintColor: '#05824C', tabBarInactiveTintColor: '#666666', + tabBarStyle: { + height: 65, paddingBottom: 8, paddingTop: 8, - height: 60, }, + tabBarLabelStyle: { fontSize: 11, fontWeight: '500', @@ -57,21 +83,25 @@ export const MainTabNavigator: React.FC = () => { component={HomeScreen} options={{ tabBarLabel: 'Home' }} /> + + + + { /> ); -}; +}; \ No newline at end of file diff --git a/package.json b/package.json index ed85aea..28b1ee9 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "react-native-screens": "^4.25.2", "react-native-svg": "^15.15.5", "react-native-uuid": "^2.0.4", + "react-native-vector-icons": "^10.3.0", "react-native-worklets": "^0.10.0", "react-redux": "^9.3.0", "reactotron-react-native": "^5.2.0", diff --git a/yarn.lock b/yarn.lock index 2597882..a2ced0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2686,6 +2686,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -5731,7 +5740,7 @@ prompts@^2.0.1, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.8.1: +prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -5894,6 +5903,14 @@ react-native-uuid@^2.0.4: resolved "https://registry.yarnpkg.com/react-native-uuid/-/react-native-uuid-2.0.4.tgz#0c4f345523c5feac0282d3a51fe19887727df988" integrity sha512-LSJNeh559qC17fgVPBsWuTSW/OygFp2dwTcf94IQBLYft5FzIQS9pCsuT36OPvyvDOMb6yiGr6TafaJDnz9PPQ== +react-native-vector-icons@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz#de440f2627a2ed1079ce3b99d5b9d4f86894df28" + integrity sha512-IFQ0RE57819hOUdFvgK4FowM5aMXg7C7XKsuGLevqXkkIJatc3QopN0wYrb2IrzUgmdpfP+QVIbI3S6h7M0btw== + dependencies: + prop-types "^15.7.2" + yargs "^16.1.1" + react-native-worklets@^0.10.0: version "0.10.1" resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.1.tgz#7a3038a3b5ec66cab030a00e1568b6599696f5f2" @@ -7064,6 +7081,11 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" @@ -7086,6 +7108,19 @@ yargs@^15.1.0: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^16.1.1: + version "16.2.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.2.tgz#c56731dca0d2788ae0866dd3c83907d6bab85f7d" + integrity sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@^17.3.1, yargs@^17.6.2: version "17.7.3" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.3.tgz#779dffe6bcafec596a7172e983289a588647faaa"