Fix form keyboard issues and sticky footers in agent screens

This commit is contained in:
Abin Toppo
2026-05-26 16:34:05 +05:30
commit 9ac5e95d52
410 changed files with 86650 additions and 0 deletions
@@ -0,0 +1,688 @@
import React, { useState, useCallback, useRef, useMemo } from 'react';
import {
StyleSheet,
View,
Text,
Pressable,
TextInput,
FlatList,
ScrollView,
Dimensions,
StatusBar,
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import MapLibreGL from '@maplibre/maplibre-react-native';
import Ionicons from '@react-native-vector-icons/ionicons';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { Loader, EmptyState } from '../../../components';
import { useTheme } from '../../../theme';
import type { ServicesStackParamList } from '../../../navigation/types';
import type { BranchFacility } from '../../../types/app';
import { useUserLocation } from '../hooks/useUserLocation';
import { useBranches, type BranchWithDistance } from '../hooks/useBranches';
import { BranchListItem } from '../components/BranchListItem';
import { BranchDetailSheet } from '../components/BranchDetailSheet';
import { FacilityFilterBar } from '../components/FacilityFilterBar';
// Initialize MapLibre (required once)
MapLibreGL.setAccessToken(null);
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const OSM_STYLE = {
version: 8,
sources: {
osm: {
type: 'raster',
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
tileSize: 256,
attribution: '© OpenStreetMap contributors',
maxzoom: 19,
},
},
layers: [
{
id: 'osm-tiles',
type: 'raster',
source: 'osm',
minzoom: 0,
maxzoom: 19,
},
],
};
type Props = NativeStackScreenProps<ServicesStackParamList, 'BranchLocator'>;
export default function BranchLocatorScreen({ navigation }: Props) {
const theme = useTheme();
const insets = useSafeAreaInsets();
// Extra bottom space to clear the tab bar (tab bar height ~60 + safe area)
const bottomOffset = Math.max(insets.bottom, 10) + 72;
// State
const [searchText, setSearchText] = useState('');
const [selectedFacilities, setSelectedFacilities] = useState<BranchFacility[]>([]);
const [selectedBranch, setSelectedBranch] = useState<BranchWithDistance | null>(null);
const [showSheet, setShowSheet] = useState(false);
const [viewMode, setViewMode] = useState<'map' | 'list'>('map');
const [filterType, setFilterType] = useState<'all' | 'branch' | 'atm'>('all');
const [selectedCity, setSelectedCity] = useState<string | null>(null);
// Hooks
const { location: userLocation, loading: locationLoading } = useUserLocation();
const { branches, isLoading, isError, refetch } = useBranches(
userLocation,
searchText,
selectedFacilities,
filterType,
selectedCity,
);
// Extract unique cities
const cities = useMemo(() => {
// We use a separate query or the hook should ideally return available cities
// For now, let's extract from the branches if available, but better to extract from raw data if possible.
// However, useBranches returns filtered ones. We need all.
// Let's just hardcode some common ones or extract if we had access to raw list.
return ['All Cities', 'Bengaluru', 'Mumbai', 'Delhi', 'Chennai', 'Hyderabad', 'Pune', 'Ahmedabad'];
}, []);
// Refs
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cameraRef = useRef<any>(null);
// Handlers
const handleFacilityToggle = useCallback((facility: BranchFacility) => {
setSelectedFacilities((prev) =>
prev.includes(facility)
? prev.filter((f) => f !== facility)
: [...prev, facility],
);
}, []);
const handleBranchPress = useCallback((branch: BranchWithDistance) => {
setSelectedBranch(branch);
setShowSheet(true);
if (viewMode === 'map' && cameraRef.current) {
cameraRef.current.setCamera({
centerCoordinate: [branch.longitude, branch.latitude],
zoomLevel: 14,
animationDuration: 600,
});
}
}, [viewMode]);
const handleCloseSheet = useCallback(() => {
setShowSheet(false);
}, []);
const handleMarkerPress = useCallback(
(branch: BranchWithDistance) => {
handleBranchPress(branch);
},
[handleBranchPress],
);
// Default map center (user location or India center)
const defaultCenter = useMemo(() => {
if (userLocation) {
return [userLocation.longitude, userLocation.latitude];
}
return [77.5946, 12.9716]; // Bengaluru fallback
}, [userLocation]);
// Loading state
if (isLoading || locationLoading) {
return (
<SafeAreaView edges={['top']} style={[styles.flex, { backgroundColor: theme.colors.background }]}>
<StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
<Header theme={theme} navigation={navigation} />
<Loader />
</SafeAreaView>
);
}
return (
<SafeAreaView edges={['top']} style={[styles.flex, { backgroundColor: theme.colors.background }]}>
<StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
{/* Header */}
<Header theme={theme} navigation={navigation} />
{/* Search Bar */}
<View style={styles.searchSection}>
<View
style={[
styles.searchBar,
{
backgroundColor: theme.colors.surface,
borderColor: theme.colors.cardBorder,
borderRadius: theme.radius.lg,
},
theme.shadows.soft,
]}
>
<Ionicons name="search-outline" size={18} color={theme.colors.textMuted} />
<TextInput
value={searchText}
onChangeText={setSearchText}
placeholder="Search city, pincode, branch..."
placeholderTextColor={theme.colors.textMuted}
style={[styles.searchInput, { color: theme.colors.textPrimary }]}
/>
{searchText.length > 0 && (
<Pressable onPress={() => setSearchText('')}>
<Ionicons name="close-circle" size={18} color={theme.colors.textMuted} />
</Pressable>
)}
</View>
{/* View Toggle */}
<Pressable
onPress={() => setViewMode((v) => (v === 'map' ? 'list' : 'map'))}
style={[
styles.toggleBtn,
{
backgroundColor: theme.colors.primary,
borderRadius: theme.radius.lg,
},
]}
>
<Ionicons
name={viewMode === 'map' ? 'list-outline' : 'map-outline'}
size={20}
color={theme.colors.white}
/>
</Pressable>
</View>
{/* Segmented Control for Locator Type */}
<View style={styles.segmentedContainer}>
{(['all', 'branch', 'atm'] as const).map((type) => {
const isActive = filterType === type;
const label = type === 'all' ? 'All' : type === 'branch' ? 'Branches' : 'ATMs';
return (
<Pressable
key={type}
onPress={() => setFilterType(type)}
style={[
styles.segmentSegment,
isActive && { backgroundColor: theme.colors.primary },
]}
>
<Text
style={[
styles.segmentLabel,
{ color: isActive ? theme.colors.white : theme.colors.textSecondary },
]}
>
{label}
</Text>
</Pressable>
);
})}
</View>
{/* Facility Filters */}
<View style={styles.filterSection}>
<FacilityFilterBar selected={selectedFacilities} onToggle={handleFacilityToggle} />
</View>
{/* City Chips */}
<View style={styles.cityFilterSection}>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.cityScroll}>
{cities.map((city) => {
const isActive = (selectedCity === city) || (!selectedCity && city === 'All Cities');
return (
<Pressable
key={city}
onPress={() => setSelectedCity(city === 'All Cities' ? null : city)}
style={[
styles.cityChip,
{
backgroundColor: isActive ? theme.colors.primary : theme.colors.surface,
borderColor: isActive ? theme.colors.primary : theme.colors.cardBorder,
},
]}
>
<Text
style={[
styles.cityChipText,
{ color: isActive ? theme.colors.white : theme.colors.textSecondary },
]}
>
{city}
</Text>
</Pressable>
);
})}
</ScrollView>
</View>
{/* Branch count */}
<View style={styles.countRow}>
<Text style={[styles.countText, { color: theme.colors.textSecondary }]}>
{branches.length} {branches.length === 1 ? 'location' : 'locations'} found
</Text>
{(selectedFacilities.length > 0 || selectedCity) && (
<Pressable onPress={() => { setSelectedFacilities([]); setSelectedCity(null); }}>
<Text style={[styles.clearText, { color: theme.colors.primary }]}>
Clear filters
</Text>
</Pressable>
)}
</View>
{/* Map or List View */}
{branches.length === 0 ? (
<View style={styles.emptyContainer}>
<EmptyState
title="No branches found"
description="Try adjusting your search or filters to find nearby branches."
actionLabel="Clear all"
onAction={() => {
setSearchText('');
setSelectedFacilities([]);
}}
/>
</View>
) : viewMode === 'map' ? (
<View style={styles.mapContainer}>
<MapLibreGL.MapView
style={styles.map}
mapStyle={OSM_STYLE}
logoEnabled={false}
attributionEnabled={true}
attributionPosition={{ bottom: 8, right: 8 }}
>
<MapLibreGL.Camera
ref={cameraRef}
defaultSettings={{
centerCoordinate: defaultCenter,
zoomLevel: 11,
}}
animationDuration={600}
/>
{/* User location marker */}
{userLocation && (
<MapLibreGL.PointAnnotation
id="user-location"
coordinate={[userLocation.longitude, userLocation.latitude]}
>
<View style={styles.userMarkerOuter}>
<View style={[styles.userMarkerInner, { backgroundColor: theme.colors.info }]} />
</View>
</MapLibreGL.PointAnnotation>
)}
{/* Branch markers */}
{branches.map((branch) => {
const isSelected = selectedBranch?.id === branch.id;
return (
<MapLibreGL.PointAnnotation
key={branch.id}
id={branch.id}
coordinate={[branch.longitude, branch.latitude]}
onSelected={() => handleMarkerPress(branch)}
>
<View
style={[
styles.markerContainer,
{
backgroundColor: isSelected
? theme.colors.primary
: theme.colors.surface,
borderColor: isSelected
? theme.colors.primaryDark
: branch.locationType === 'atm' ? theme.colors.info : theme.colors.primary,
transform: [{ scale: isSelected ? 1.2 : 1 }],
},
]}
>
<Ionicons
name={branch.locationType === 'atm' ? 'card' : branch.isMainBranch ? 'business' : 'location'}
size={16}
color={isSelected ? theme.colors.white : branch.locationType === 'atm' ? theme.colors.info : theme.colors.primary}
/>
</View>
<MapLibreGL.Callout title={branch.name} />
</MapLibreGL.PointAnnotation>
);
})}
</MapLibreGL.MapView>
{/* Nearest branch floating card */}
{(() => {
const nearest = branches[0];
if (!nearest || nearest.distance === null || showSheet) { return null; }
return (
<Pressable
style={[
styles.nearestCard,
{
backgroundColor: theme.colors.surface,
borderRadius: theme.radius.lg,
bottom: bottomOffset,
},
theme.shadows.soft,
]}
onPress={() => handleBranchPress(nearest)}
>
<View style={[styles.nearestIcon, { backgroundColor: `${theme.colors.accent}14` }]}>
<Ionicons name="navigate" size={16} color={theme.colors.accent} />
</View>
<View style={styles.nearestContent}>
<Text style={[styles.nearestLabel, { color: theme.colors.textMuted }]}>
NEAREST {nearest.locationType === 'atm' ? 'ATM' : 'BRANCH'}
</Text>
<Text
style={[styles.nearestName, { color: theme.colors.textPrimary }]}
numberOfLines={1}
>
{nearest.name}
</Text>
<Text style={[styles.nearestDistance, { color: theme.colors.textSecondary }]}>
{nearest.distance !== null
? `${nearest.distance.toFixed(1)} km away`
: ''}
</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={theme.colors.textMuted} />
</Pressable>
);
})()}
{/* Re-center button */}
{userLocation && (
<Pressable
style={[
styles.recenterBtn,
{
backgroundColor: theme.colors.surface,
borderRadius: theme.radius.pill,
bottom: bottomOffset + 80,
},
theme.shadows.soft,
]}
onPress={() => {
cameraRef.current?.setCamera({
centerCoordinate: [userLocation.longitude, userLocation.latitude],
zoomLevel: 12,
animationDuration: 600,
});
}}
>
<Ionicons name="locate" size={20} color={theme.colors.primary} />
</Pressable>
)}
</View>
) : (
/* List View */
<FlatList
data={branches}
keyExtractor={(item) => item.id}
contentContainerStyle={[styles.listContent, { paddingBottom: bottomOffset }]}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<BranchListItem
branch={item}
isSelected={selectedBranch?.id === item.id}
onPress={() => handleBranchPress(item)}
/>
)}
/>
)}
{/* Branch Detail Bottom Sheet */}
<BranchDetailSheet
branch={selectedBranch}
visible={showSheet}
onClose={handleCloseSheet}
/>
</SafeAreaView>
);
}
// ── Header sub-component ──
interface HeaderProps {
theme: ReturnType<typeof useTheme>;
navigation: Props['navigation'];
}
const Header = ({ theme, navigation }: HeaderProps) => (
<View style={styles.header}>
<Pressable
onPress={() => navigation.goBack()}
style={[
styles.backButton,
{
backgroundColor: theme.colors.cardGlass,
borderColor: theme.colors.cardBorder,
borderRadius: theme.radius.pill,
},
]}
>
<Ionicons name="chevron-back" size={20} color={theme.colors.textPrimary} />
</Pressable>
<View style={styles.headerCopy}>
<Text style={[styles.headerTitle, { color: theme.colors.textPrimary }]}>
BRANCH & ATM LOCATOR
</Text>
</View>
</View>
);
// ── Styles ──
const styles = StyleSheet.create({
flex: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
paddingHorizontal: 10,
paddingVertical: 8,
},
backButton: {
width: 42,
height: 42,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
},
headerCopy: {
flex: 1,
},
headerTitle: {
fontSize: 20,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
lineHeight: 20,
},
searchSection: {
flexDirection: 'row',
paddingHorizontal: 10,
gap: 10,
marginBottom: 8,
},
searchBar: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
height: 46,
paddingHorizontal: 14,
borderWidth: 1,
gap: 8,
},
searchInput: {
flex: 1,
fontSize: 14,
fontWeight: '500',
height: '100%',
},
toggleBtn: {
width: 46,
height: 46,
alignItems: 'center',
justifyContent: 'center',
},
filterSection: {
paddingHorizontal: 6,
marginBottom: 6,
},
segmentedContainer: {
flexDirection: 'row',
marginHorizontal: 10,
marginBottom: 8,
padding: 4,
backgroundColor: 'rgba(0,0,0,0.05)',
borderRadius: 12,
},
segmentSegment: {
flex: 1,
paddingVertical: 8,
alignItems: 'center',
borderRadius: 8,
},
segmentLabel: {
fontSize: 12,
fontWeight: '600',
},
countRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 14,
marginBottom: 6,
},
countText: {
fontSize: 12,
fontWeight: '600',
},
clearText: {
fontSize: 12,
fontWeight: '700',
},
mapContainer: {
flex: 1,
position: 'relative',
},
map: {
flex: 1,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
paddingHorizontal: 32,
},
listContent: {
paddingHorizontal: 10,
paddingBottom: 30,
},
// Markers
markerContainer: {
width: 36,
height: 36,
borderRadius: 18,
borderWidth: 2,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 4,
elevation: 4,
},
userMarkerOuter: {
width: 22,
height: 22,
borderRadius: 11,
backgroundColor: 'rgba(59, 130, 246, 0.2)',
alignItems: 'center',
justifyContent: 'center',
},
userMarkerInner: {
width: 12,
height: 12,
borderRadius: 6,
borderWidth: 2,
borderColor: '#FFFFFF',
},
// Nearest Branch Card
nearestCard: {
position: 'absolute',
bottom: 20,
left: 14,
right: 14,
flexDirection: 'row',
alignItems: 'center',
padding: 14,
gap: 10,
},
nearestIcon: {
width: 38,
height: 38,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
nearestContent: {
flex: 1,
gap: 1,
},
cityFilterSection: {
paddingHorizontal: 16,
marginBottom: 8,
},
cityScroll: {
gap: 8,
},
cityChip: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 20,
borderWidth: 1,
},
cityChipText: {
fontSize: 12,
fontWeight: '600',
},
sectionCopy: {
flex: 1,
gap: 1,
},
nearestLabel: {
fontSize: 9,
fontWeight: '700',
letterSpacing: 0.8,
},
nearestName: {
fontSize: 14,
fontWeight: '700',
},
nearestDistance: {
fontSize: 12,
},
// Re-center Button
recenterBtn: {
position: 'absolute',
right: 14,
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
},
});
@@ -0,0 +1,219 @@
import React, { useState } from 'react';
import { View, StyleSheet, Text, FlatList } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import Ionicons from '@react-native-vector-icons/ionicons';
import type { ServicesStackParamList } from '../../../navigation/types';
import { ScreenShell, Card, Button } from '../../../components';
import { AppModal } from '../../../components/Modal';
import { useToast } from '../../../hooks/useToast';
import { useTheme } from '../../../theme';
type Props = NativeStackScreenProps<ServicesStackParamList, 'CertificateList'>;
export default function CertificateListScreen({ route }: Props) {
const { title, category } = route.params;
const theme = useTheme();
const toast = useToast();
const [viewingDoc, setViewingDoc] = useState<string | null>(null);
const mockDocs = Array.from({ length: 4 }).map((_, i) => ({
id: String(i),
name: `FY ${26 - i}-${27 - i} ${title}`,
date: `Generated Apr ${2026 - i}`,
size: `${(1.2 + i * 0.3).toFixed(1)} MB`,
}));
const handleDownload = (docName: string) => {
toast('Download Started', 'success', `${docName} is being securely downloaded to your device.`);
};
return (
<ScreenShell title={title} subtitle={`${mockDocs.length} documents available`}>
<FlatList
data={mockDocs}
keyExtractor={item => item.id}
contentContainerStyle={{ gap: 16, paddingBottom: 24 }}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<Card variant="glass" style={styles.card}>
<View style={styles.cardTop}>
<View style={[styles.docIcon, { backgroundColor: `${theme.colors.primary}10`, borderColor: `${theme.colors.primary}20` }]}>
<Ionicons name="document-text" size={26} color={theme.colors.primary} />
</View>
<View style={styles.docInfo}>
<Text style={[styles.docTitle, { color: theme.colors.textPrimary }]}>{item.name}</Text>
<Text style={[styles.docMeta, { color: theme.colors.textSecondary }]}>
{item.date} • {item.size}
</Text>
</View>
</View>
<View style={[styles.divider, { backgroundColor: theme.colors.cardBorder }]} />
<View style={styles.actionRow}>
<Button
title="View"
variant="ghost"
fullWidth={false}
style={[styles.actionBtn, { borderColor: theme.colors.cardBorder }]}
onPress={() => setViewingDoc(item.name)}
leftAccessory={<Ionicons name="eye-outline" size={18} color={theme.colors.textPrimary} style={{ marginRight: 2 }} />}
/>
<Button
title="Download"
variant="secondary"
fullWidth={false}
style={styles.actionBtn}
onPress={() => handleDownload(item.name)}
leftAccessory={<Ionicons name="cloud-download-outline" size={18} color={theme.colors.textPrimary} style={{ marginRight: 2 }} />}
/>
</View>
</Card>
)}
/>
<AppModal
visible={!!viewingDoc}
title="Document Preview"
onClose={() => setViewingDoc(null)}
>
<View style={[styles.certificatePreview, { backgroundColor: theme.colors.background, borderColor: theme.colors.cardBorder }]}>
<View style={[styles.certHeader, { borderBottomColor: theme.colors.cardBorder }]}>
<Ionicons name="business" size={20} color={theme.colors.primary} />
<Text style={[styles.certBankName, { color: theme.colors.primary }]}>CBS BANK LTD.</Text>
</View>
<Text style={[styles.certTitle, { color: theme.colors.textPrimary }]}>{title}</Text>
<View style={{ gap: 8, marginTop: 12 }}>
<Text style={[styles.certText, { color: theme.colors.textSecondary }]}>
This officially certifies that the document <Text style={{fontWeight: '700', color: theme.colors.textPrimary}}>{viewingDoc}</Text> has been cryptographically generated and is valid.
</Text>
<Text style={[styles.certText, { color: theme.colors.textSecondary }]}>Reference Category: <Text style={{fontWeight: '700'}}>{category.toUpperCase()}</Text></Text>
<Text style={[styles.certText, { color: theme.colors.textSecondary }]}>Account Bearer: Lincpay User</Text>
<Text style={[styles.certText, { color: theme.colors.textSecondary }]}>Account #: XXXX-XXXX-1234</Text>
</View>
<View style={styles.certFooter}>
<View style={[styles.sealBox, { borderColor: theme.colors.primary }]}>
<Ionicons name="ribbon-outline" size={20} color={theme.colors.primary} />
</View>
<View style={{ alignItems: 'center' }}>
<Text style={[styles.signature, { color: theme.colors.textPrimary }]}>Auth. Signatory</Text>
<View style={{ width: 80, height: 1, backgroundColor: theme.colors.textSecondary, marginTop: 4 }} />
</View>
</View>
</View>
<Button
title="Download Full PDF"
variant="primary"
onPress={() => {
if (viewingDoc) handleDownload(viewingDoc);
setViewingDoc(null);
}}
style={{ marginTop: 8 }}
leftAccessory={<Ionicons name="cloud-download-outline" size={18} color="#fff" style={{ marginRight: 4 }} />}
/>
<Button
title="Close"
variant="ghost"
onPress={() => setViewingDoc(null)}
style={{ marginTop: 4 }}
/>
</AppModal>
</ScreenShell>
);
}
const styles = StyleSheet.create({
card: {
padding: 16,
gap: 16,
borderWidth: 1,
},
cardTop: {
flexDirection: 'row',
alignItems: 'center',
gap: 16,
},
docIcon: {
width: 52,
height: 52,
borderRadius: 16,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
},
docInfo: {
flex: 1,
gap: 6,
},
docTitle: {
fontWeight: '700',
fontSize: 16,
letterSpacing: -0.3,
},
docMeta: {
fontSize: 13,
fontWeight: '500',
},
divider: {
height: 1,
width: '100%',
opacity: 0.6,
},
actionRow: {
flexDirection: 'row',
gap: 12,
},
actionBtn: {
flex: 1,
height: 44,
},
certificatePreview: {
padding: 20,
borderWidth: 1,
borderRadius: 8,
marginVertical: 12,
},
certHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
borderBottomWidth: 1,
paddingBottom: 8,
marginBottom: 12,
},
certBankName: {
fontWeight: '900',
fontSize: 14,
letterSpacing: 0.5,
},
certTitle: {
fontWeight: '800',
fontSize: 18,
textAlign: 'center',
},
certText: {
fontSize: 13,
lineHeight: 20,
},
certFooter: {
marginTop: 24,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-end',
},
sealBox: {
width: 44,
height: 44,
borderRadius: 22,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
borderStyle: 'dashed',
},
signature: {
fontStyle: 'italic',
fontSize: 14,
fontWeight: '600',
},
});
@@ -0,0 +1,166 @@
import React, { useMemo } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import Ionicons from '@react-native-vector-icons/ionicons';
import type { ServicesStackParamList } from '../../../navigation/types';
import { ScreenShell, Card } from '../../../components';
import { ServiceGridItem } from '../../home/components/ServiceGridItem';
import { useTheme } from '../../../theme';
type Props = NativeStackScreenProps<ServicesStackParamList, 'CertificatesHub'>;
export default function CertificatesHubScreen({ navigation }: Props) {
const theme = useTheme();
const options = useMemo(
() => [
{
id: 'balance',
title: 'Balance Cert',
icon: 'wallet-outline',
iconColor: '#3B82F6',
onPress: () => navigation.navigate('CertificateList', { title: 'Balance Certificates', category: 'balance' }),
},
{
id: 'interest',
title: 'Interest Cert',
icon: 'trending-up-outline',
iconColor: '#10B981',
onPress: () => navigation.navigate('CertificateList', { title: 'Interest Certificates', category: 'interest' }),
},
{
id: 'hypothecation',
title: 'Hypothecation',
icon: 'document-text-outline',
iconColor: '#F59E0B',
onPress: () => navigation.navigate('CertificateList', { title: 'Hypothecation', category: 'hypothecation' }),
},
{
id: 'insurance',
title: 'Insurance',
icon: 'shield-checkmark-outline',
iconColor: '#8B5CF6',
onPress: () => navigation.navigate('CertificateList', { title: 'Insurance Copies', category: 'insurance' }),
},
{
id: 'tax',
title: 'Tax Forms',
icon: 'receipt-outline',
iconColor: '#EC4899',
onPress: () => navigation.navigate('CertificateList', { title: 'Tax Statements', category: 'tax' }),
},
{
id: 'loan',
title: 'Loan Docs',
icon: 'home-outline',
iconColor: '#6366F1',
onPress: () => navigation.navigate('CertificateList', { title: 'Loan Documents', category: 'loan' }),
},
],
[navigation]
);
return (
<ScreenShell title="Certificates" subtitle="Secure document vault">
<Card variant="glass" style={styles.heroCard}>
<View style={styles.heroHeader}>
<View style={[styles.heroIcon, { backgroundColor: `${theme.colors.primary}15` }]}>
<Ionicons name="folder-open-outline" size={24} color={theme.colors.primary} />
</View>
<View style={styles.heroCopy}>
<Text style={[styles.heroTitle, { color: theme.colors.textPrimary }]}>Document Vault</Text>
<Text style={[styles.heroSubtitle, { color: theme.colors.textSecondary }]}>
All your official bank certificates and statements, generated dynamically and secured.
</Text>
</View>
</View>
<View style={styles.heroStats}>
<View style={[styles.heroStat, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.heroStatValue, { color: theme.colors.textPrimary }]}>12</Text>
<Text style={[styles.heroStatLabel, { color: theme.colors.textSecondary }]}>Generated</Text>
</View>
<View style={[styles.heroStat, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.heroStatValue, { color: theme.colors.primary }]}>Active</Text>
<Text style={[styles.heroStatLabel, { color: theme.colors.textSecondary }]}>Vault Status</Text>
</View>
</View>
</Card>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>Select Category</Text>
<View style={styles.grid}>
{options.map((opt) => (
<ServiceGridItem key={opt.id} {...opt} />
))}
</View>
</ScreenShell>
);
}
const styles = StyleSheet.create({
heroCard: {
padding: 20,
gap: 20,
marginBottom: 8,
},
heroHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 16,
},
heroIcon: {
width: 48,
height: 48,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
heroCopy: {
flex: 1,
gap: 4,
},
heroTitle: {
fontSize: 18,
fontWeight: '800',
letterSpacing: -0.3,
},
heroSubtitle: {
fontSize: 13,
lineHeight: 18,
},
heroStats: {
flexDirection: 'row',
gap: 12,
},
heroStat: {
flex: 1,
borderRadius: 12,
padding: 12,
alignItems: 'center',
justifyContent: 'center',
gap: 4,
},
heroStatLabel: {
fontSize: 11,
fontWeight: '600',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
heroStatValue: {
fontSize: 18,
fontWeight: '800',
},
sectionTitle: {
fontSize: 16,
fontWeight: '700',
marginTop: 8,
marginLeft: 4,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
});
@@ -0,0 +1,62 @@
import React, { useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import type { ServicesStackParamList } from '../../../navigation/types';
import { ScreenShell, Card, Input, Button } from '../../../components';
import { useToast } from '../../../hooks/useToast';
import { useTheme } from '../../../theme';
type Props = NativeStackScreenProps<ServicesStackParamList, 'ChequeBookRequest'>;
export default function ChequeBookRequestScreen({ navigation }: Props) {
const theme = useTheme();
const toast = useToast();
const [account, setAccount] = useState('0000XXXX1234');
const [leaves, setLeaves] = useState('25');
const [address, setAddress] = useState('Registered Address');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
toast('Request Submitted', 'success', 'Your cheque book will be delivered in 3-5 days.');
navigation.goBack();
}, 1500);
};
return (
<ScreenShell title="Cheque Book" subtitle="Request a new cheque book">
<Card variant="default" style={styles.card}>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Select Account</Text>
<Input value={account} onChangeText={setAccount} placeholder="Account Number" />
</View>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Number of Leaves</Text>
<Input value={leaves} onChangeText={setLeaves} placeholder="e.g. 25, 50, 100" keyboardType="number-pad" />
</View>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Delivery Address</Text>
<Input value={address} onChangeText={setAddress} placeholder="Branch or Registered Address" />
</View>
<Button
title="Submit Request"
onPress={handleSubmit}
loading={isLoading}
style={styles.submitBtn}
/>
</Card>
</ScreenShell>
);
}
const styles = StyleSheet.create({
card: { padding: 16, gap: 16 },
formGroup: { gap: 8 },
label: { fontSize: 13, fontWeight: '600' },
submitBtn: { marginTop: 16 },
});
@@ -0,0 +1,93 @@
import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import type { ServicesStackParamList } from '../../../navigation/types';
import { ScreenShell } from '../../../components';
import { ServiceGridItem } from '../../home/components/ServiceGridItem';
import { useTheme } from '../../../theme';
import { ChequeAnimation } from '../components/ChequeAnimation';
type Props = NativeStackScreenProps<ServicesStackParamList, 'ChequeServicesHub'>;
export default function ChequeServicesHubScreen({ navigation }: Props) {
const theme = useTheme();
const options = useMemo(
() => [
{
id: 'request',
title: 'Book Request',
icon: 'book-outline',
iconColor: '#3B82F6',
onPress: () => navigation.navigate('ChequeBookRequest'),
},
{
id: 'stop',
title: 'Stop Payment',
icon: 'stop-circle-outline',
iconColor: '#EF4444',
onPress: () => navigation.navigate('StopPayment'),
},
{
id: 'track',
title: 'Track Cheques',
icon: 'git-compare-outline',
iconColor: '#10B981',
onPress: () => navigation.navigate('ChequeTracking'),
},
],
[navigation]
);
return (
<ScreenShell title="Cheque Services" subtitle="Manage all your cheque-related needs">
{/* Animated cheque illustration */}
<ChequeAnimation />
{/* Quick info strip */}
<View style={[styles.infoStrip, { backgroundColor: `${theme.colors.primary}08`, borderColor: `${theme.colors.primary}12` }]}>
<View style={[styles.infoDot, { backgroundColor: theme.colors.accent }]} />
<Text style={[styles.infoText, { color: theme.colors.textSecondary }]}>
Request, track, or manage cheques — all in one place
</Text>
</View>
{/* Service grid */}
<View style={styles.grid}>
{options.map((opt) => (
<ServiceGridItem key={opt.id} {...opt} />
))}
</View>
</ScreenShell>
);
}
const styles = StyleSheet.create({
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
paddingTop: 8,
},
infoStrip: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
borderRadius: 12,
borderWidth: 1,
paddingHorizontal: 14,
paddingVertical: 12,
marginBottom: 16,
},
infoDot: {
width: 6,
height: 6,
borderRadius: 3,
},
infoText: {
fontSize: 12,
fontWeight: '600',
flex: 1,
lineHeight: 17,
},
});
@@ -0,0 +1,45 @@
import React from 'react';
import { View, StyleSheet, Text, FlatList } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import type { ServicesStackParamList } from '../../../navigation/types';
import { ScreenShell, Card } from '../../../components';
import { useTheme } from '../../../theme';
type Props = NativeStackScreenProps<ServicesStackParamList, 'ChequeTracking'>;
const MOCK_TRACKING = [
{ id: '1', title: 'Cheque Book #65', status: 'Delivered', date: '10 Oct 2026', items: [{ stage: 'Requested', time: '01 Oct', active: false }, { stage: 'Processed', time: '05 Oct', active: false }, { stage: 'Delivered', time: '10 Oct', active: true }] },
{ id: '2', title: 'Cheque Book #66', status: 'Processing', date: 'Today', items: [{ stage: 'Requested', time: 'Today', active: false }, { stage: 'Processing', time: 'Today', active: true }] }
];
export default function ChequeTrackingScreen({ navigation }: Props) {
const theme = useTheme();
return (
<ScreenShell title="Track Cheques" subtitle="Cheque books and individual cheques">
<FlatList
data={MOCK_TRACKING}
keyExtractor={item => item.id}
contentContainerStyle={{ gap: 16, paddingBottom: 24 }}
renderItem={({ item }) => (
<Card style={{ padding: 16, gap: 12 }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={{ fontWeight: '700', fontSize: 16, color: theme.colors.textPrimary }}>{item.title}</Text>
<Text style={{ fontSize: 13, color: theme.colors.textSecondary }}>{item.date}</Text>
</View>
<View style={{ height: 1, backgroundColor: `${theme.colors.cardBorder}40`, marginVertical: 4 }} />
{item.items.map((stage, idx) => (
<View key={idx} style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
<View style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: stage.active ? theme.colors.primary : theme.colors.cardBorder }} />
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-between' }}>
<Text style={{ color: stage.active ? theme.colors.primary : theme.colors.textSecondary, fontWeight: stage.active ? '700' : '500' }}>{stage.stage}</Text>
<Text style={{ fontSize: 12, color: theme.colors.textMuted }}>{stage.time}</Text>
</View>
</View>
))}
</Card>
)}
/>
</ScreenShell>
);
}
@@ -0,0 +1,93 @@
import React, { useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import type { ServicesStackParamList } from '../../../navigation/types';
import { ScreenShell, Card, Input, Button } from '../../../components';
import { useToast } from '../../../hooks/useToast';
import { useTheme } from '../../../theme';
type Props = NativeStackScreenProps<ServicesStackParamList, 'Form15G15H'>;
export default function Form15G15HScreen({ navigation }: Props) {
const theme = useTheme();
const toast = useToast();
const [formType, setFormType] = useState('15G');
const [panNumber, setPanNumber] = useState('');
const [estimatedIncome, setEstimatedIncome] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = () => {
if (!panNumber || !estimatedIncome) {
toast('Validation Failed', 'error', 'PAN and estimated income are required.');
return;
}
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
toast('Form Submitted successfully', 'success', `Your Form ${formType} has been recorded for the current FY.`);
navigation.goBack();
}, 1500);
};
return (
<ScreenShell title="Form 15G / 15H" subtitle="Submit your tax declarations">
<Card variant="default" style={styles.card}>
<View style={{ flexDirection: 'row', gap: 12, marginBottom: 8 }}>
<Button
title="Form 15G"
variant={formType === '15G' ? 'primary' : 'secondary'}
onPress={() => setFormType('15G')}
style={{ flex: 1 }}
/>
<Button
title="Form 15H (Senior Citizens)"
variant={formType === '15H' ? 'primary' : 'secondary'}
onPress={() => setFormType('15H')}
style={{ flex: 1 }}
/>
</View>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>PAN Number</Text>
<Input value={panNumber} onChangeText={setPanNumber} placeholder="ABCDE1234F" autoCapitalize="characters" />
</View>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Estimated Income (₹)</Text>
<Input value={estimatedIncome} onChangeText={setEstimatedIncome} placeholder="e.g. 350000" keyboardType="numeric" />
</View>
<View style={{ padding: 12, backgroundColor: `${theme.colors.info}15`, borderRadius: 8 }}>
<Text style={{ color: theme.colors.info, fontSize: 13, lineHeight: 18 }}>
By submitting, you declare that the tax on your estimated total income for the previous year will be nil.
</Text>
</View>
<Button
title={`Submit Form ${formType}`}
onPress={handleSubmit}
loading={isLoading}
style={styles.submitBtn}
/>
</Card>
<View style={{ marginTop: 24, gap: 16 }}>
<Text style={{ fontWeight: '700', fontSize: 16, color: theme.colors.textPrimary, marginLeft: 4 }}>History</Text>
<Card style={{ padding: 16, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View>
<Text style={{ fontWeight: '700', color: theme.colors.textPrimary }}>Form 15G - FY 24-25</Text>
<Text style={{ fontSize: 12, color: theme.colors.textSecondary }}>Submitted April 2025</Text>
</View>
<Button title="Download" variant="ghost" onPress={() => toast('Downloading...', 'info')} />
</Card>
</View>
</ScreenShell>
);
}
const styles = StyleSheet.create({
card: { padding: 16, gap: 16 },
formGroup: { gap: 8 },
label: { fontSize: 13, fontWeight: '600' },
submitBtn: { marginTop: 8 },
});
@@ -0,0 +1,78 @@
import React, { useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import type { ServicesStackParamList } from '../../../navigation/types';
import { ScreenShell, Card, Input, Button } from '../../../components';
import { useToast } from '../../../hooks/useToast';
import { useTheme } from '../../../theme';
type Props = NativeStackScreenProps<ServicesStackParamList, 'PositivePay'>;
export default function PositivePayScreen({ navigation }: Props) {
const theme = useTheme();
const toast = useToast();
const [account, setAccount] = useState('0000XXXX1234');
const [chequeNumber, setChequeNumber] = useState('');
const [amount, setAmount] = useState('');
const [payee, setPayee] = useState('');
const [date, setDate] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = () => {
if (!chequeNumber || !amount || !payee) {
toast('Incomplete Form', 'error', 'Please fill all required details.');
return;
}
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
toast('Verification Submitted', 'success', `Details for Cheque #${chequeNumber} saved securely.`);
navigation.goBack();
}, 1500);
};
return (
<ScreenShell title="Positive Pay System" subtitle="Secure your high-value cheques">
<Card variant="default" style={styles.card}>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Select Account</Text>
<Input value={account} onChangeText={setAccount} placeholder="Account Number" />
</View>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Cheque Number</Text>
<Input value={chequeNumber} onChangeText={setChequeNumber} placeholder="6-digit unique number" keyboardType="number-pad" />
</View>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Amount</Text>
<Input value={amount} onChangeText={setAmount} placeholder="e.g. 50000" keyboardType="numeric" />
</View>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Payee Name</Text>
<Input value={payee} onChangeText={setPayee} placeholder="Exact name on cheque" />
</View>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Cheque Date</Text>
<Input value={date} onChangeText={setDate} placeholder="DD/MM/YYYY" />
</View>
<Button
title="Submit Details"
onPress={handleSubmit}
loading={isLoading}
style={styles.submitBtn}
/>
</Card>
</ScreenShell>
);
}
const styles = StyleSheet.create({
card: { padding: 16, gap: 16 },
formGroup: { gap: 8 },
label: { fontSize: 13, fontWeight: '600' },
submitBtn: { marginTop: 16 },
});
@@ -0,0 +1,93 @@
import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import type { ServicesStackParamList } from '../../../navigation/types';
import { ScreenShell } from '../../../components';
import { ServiceGridItem } from '../../home/components/ServiceGridItem';
import { useTheme } from '../../../theme';
import { ServiceRequestAnimation } from '../components/ServiceRequestAnimation';
type Props = NativeStackScreenProps<ServicesStackParamList, 'ServiceRequestsHub'>;
export default function ServiceRequestsHubScreen({ navigation }: Props) {
const theme = useTheme();
const options = useMemo(
() => [
{
id: 'positive_pay',
title: 'Positive Pay',
icon: 'lock-closed-outline',
iconColor: '#F43F5E',
onPress: () => navigation.navigate('PositivePay'),
},
{
id: 'form15g',
title: 'Form 15G/15H',
icon: 'document-attach-outline',
iconColor: '#10B981',
onPress: () => navigation.navigate('Form15G15H'),
},
{
id: 'special',
title: 'Special Instr.',
icon: 'create-outline',
iconColor: '#6366F1',
onPress: () => navigation.navigate('SpecialInstructions'),
},
],
[navigation]
);
return (
<ScreenShell title="Service Requests" subtitle="Submit and track requests">
{/* Animated illustration */}
<ServiceRequestAnimation />
{/* Info strip */}
<View style={[styles.infoStrip, { backgroundColor: `${theme.colors.info}08`, borderColor: `${theme.colors.info}12` }]}>
<View style={[styles.infoDot, { backgroundColor: theme.colors.info }]} />
<Text style={[styles.infoText, { color: theme.colors.textSecondary }]}>
Submit forms, set up Positive Pay, or add special instructions
</Text>
</View>
{/* Service grid */}
<View style={styles.grid}>
{options.map((opt) => (
<ServiceGridItem key={opt.id} {...opt} />
))}
</View>
</ScreenShell>
);
}
const styles = StyleSheet.create({
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
paddingTop: 8,
},
infoStrip: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
borderRadius: 12,
borderWidth: 1,
paddingHorizontal: 14,
paddingVertical: 12,
marginBottom: 16,
},
infoDot: {
width: 6,
height: 6,
borderRadius: 3,
},
infoText: {
fontSize: 12,
fontWeight: '600',
flex: 1,
lineHeight: 17,
},
});
@@ -0,0 +1,260 @@
import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import type { CompositeScreenProps } from '@react-navigation/native';
import type { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import Ionicons from '@react-native-vector-icons/ionicons';
import { useAppSelector } from '../../../app/hooks';
import { Card, ScreenShell } from '../../../components';
import { useToast } from '../../../hooks/useToast';
import { ServiceGridItem } from '../../home/components/ServiceGridItem';
import { ServiceStatusPill } from '../components/ServiceStatusPill';
import { selectServiceRequests } from '../store/serviceSelectors';
import type { MainTabParamList, ServicesStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
type Props = CompositeScreenProps<
NativeStackScreenProps<ServicesStackParamList, 'ServicesHub'>,
BottomTabScreenProps<MainTabParamList, 'Services'>
>;
export default function ServicesHubScreen({ navigation }: Props) {
const theme = useTheme();
const toast = useToast();
const serviceRequests = useAppSelector(selectServiceRequests);
const latestRequest = serviceRequests[0] ?? null;
const openRequestsCount = serviceRequests.filter(
(request) => request.status !== 'resolved',
).length;
const serviceOptions = useMemo(
() => [
{
id: 'branch_locator',
title: 'Find Branch & ATMs',
icon: 'map-outline',
iconColor: '#10B981',
onPress: () => navigation.navigate('BranchLocator'),
},
{
id: 'all_deposits',
title: 'Deposits',
icon: 'wallet-outline',
iconColor: '#124577',
onPress: () => navigation.navigate('DepositsOverview'),
},
{
id: 'cheque_services',
title: 'Cheque Services',
icon: 'book-outline',
iconColor: '#3B82F6',
onPress: () => navigation.navigate('ChequeServicesHub'),
},
{
id: 'certificates',
title: 'Certificates',
icon: 'document-text-outline',
iconColor: '#F59E0B',
onPress: () => navigation.navigate('CertificatesHub'),
},
{
id: 'requests',
title: 'Service Requests',
icon: 'git-pull-request-outline',
iconColor: '#EF4444',
onPress: () => navigation.navigate('ServiceRequestsHub'),
},
{
id: 'calculator',
title: 'FD/RD Calc',
icon: 'calculator-outline',
iconColor: '#6366F1',
onPress: () => navigation.navigate('DepositCalculator'),
},
{
id: 'support',
title: 'Support Chat',
icon: 'chatbubble-ellipses-outline',
iconColor: '#0EA5E9',
onPress: () =>
toast(
'Support chat screen pending',
'info',
'This feature is coming in the next update.',
),
},
{
id: 'help',
title: 'Help Center',
icon: 'help-circle-outline',
iconColor: '#8B5CF6',
onPress: () =>
toast('Help center pending', 'info', 'Coming soon.'),
},
{
id: 'cyber_security',
title: 'Cyber Security',
icon: 'shield-half-outline',
iconColor: '#F59E0B',
onPress: () => navigation.navigate('AwarenessList'),
},
{
id: 'protection_schemes',
title: 'Protection & Schemes',
icon: 'umbrella-outline',
iconColor: '#EC4899',
onPress: () => navigation.navigate('FinancialProtectionMain'),
},
],
[navigation, toast]
);
return (
<ScreenShell title="Service desk" subtitle="All service options in one hub">
{/* <View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Service options
</Text>
<Text style={[styles.sectionSubtitle, { color: theme.colors.textSecondary }]}>
Choose a service card to continue
</Text>
</View> */}
<View style={styles.grid}>
{serviceOptions.map((option) => (
<ServiceGridItem
key={option.id}
id={option.id}
title={option.title}
icon={option.icon}
iconColor={option.iconColor}
onPress={option.onPress}
/>
))}
</View>
<Card variant="glass" style={styles.heroCard}>
<View style={styles.heroHeader}>
<View
style={[
styles.heroIcon,
{ backgroundColor: `${theme.colors.primary}12` },
]}
>
<Ionicons name="grid-outline" size={18} color={theme.colors.primary} />
</View>
<View style={styles.heroCopy}>
<Text style={[styles.heroTitle, { color: theme.colors.textPrimary }]}>
Services hub
</Text>
<Text style={[styles.heroSubtitle, { color: theme.colors.textSecondary }]}>
Browse deposit journeys, service support actions, and request tools from one place.
</Text>
</View>
</View>
<View style={styles.heroStats}>
<View style={[styles.heroStat, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.heroStatLabel, { color: theme.colors.textSecondary }]}>
Open requests
</Text>
<Text style={[styles.heroStatValue, { color: theme.colors.textPrimary }]}>
{openRequestsCount}
</Text>
</View>
<View style={[styles.heroStat, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.heroStatLabel, { color: theme.colors.textSecondary }]}>
Latest status
</Text>
{latestRequest ? (
<ServiceStatusPill status={latestRequest.status} />
) : (
<Text style={[styles.heroStatEmpty, { color: theme.colors.textMuted }]}>
No requests
</Text>
)}
</View>
</View>
</Card>
<View style={styles.bottomSpacer} />
</ScreenShell>
);
}
const styles = StyleSheet.create({
heroCard: {
gap: 16,
shadowOpacity: 0.04,
shadowRadius: 14,
elevation: 2,
},
heroHeader: {
flexDirection: 'row',
gap: 12,
},
heroIcon: {
width: 42,
height: 42,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
heroCopy: {
flex: 1,
gap: 4,
},
heroTitle: {
fontSize: 18,
fontWeight: '800',
},
heroSubtitle: {
fontSize: 13,
lineHeight: 18,
},
heroStats: {
flexDirection: 'row',
gap: 12,
},
heroStat: {
flex: 1,
borderRadius: 18,
padding: 14,
gap: 6,
},
heroStatLabel: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.8,
},
heroStatValue: {
fontSize: 22,
fontWeight: '900',
letterSpacing: -0.5,
},
heroStatEmpty: {
fontSize: 13,
fontWeight: '600',
},
sectionHeader: {
gap: 4,
},
sectionTitle: {
fontSize: 18,
fontWeight: '800',
},
sectionSubtitle: {
fontSize: 13,
lineHeight: 18,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
bottomSpacer: {
height: 60,
},
});
@@ -0,0 +1,65 @@
import React, { useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import type { ServicesStackParamList } from '../../../navigation/types';
import { ScreenShell, Card, Input, Button } from '../../../components';
import { useToast } from '../../../hooks/useToast';
import { useTheme } from '../../../theme';
type Props = NativeStackScreenProps<ServicesStackParamList, 'SpecialInstructions'>;
export default function SpecialInstructionsScreen({ navigation }: Props) {
const theme = useTheme();
const toast = useToast();
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = () => {
if (!title || !description) {
toast('Empty fields', 'error', 'Title and description are required.');
return;
}
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
toast('Request Logged', 'success', 'Your special instruction has been submitted to the branch.');
navigation.goBack();
}, 1500);
};
return (
<ScreenShell title="Special Instructions" subtitle="Submit custom service requests">
<Card variant="default" style={styles.card}>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Request Title</Text>
<Input value={title} onChangeText={setTitle} placeholder="Short objective summary" />
</View>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Description</Text>
<Input
value={description}
onChangeText={setDescription}
placeholder="Detailed instructions..."
multiline
/>
</View>
<Button
title="Submit Instruction"
onPress={handleSubmit}
loading={isLoading}
style={styles.submitBtn}
/>
</Card>
</ScreenShell>
);
}
const styles = StyleSheet.create({
card: { padding: 16, gap: 16 },
formGroup: { gap: 8 },
label: { fontSize: 13, fontWeight: '600' },
submitBtn: { marginTop: 16 },
});
@@ -0,0 +1,67 @@
import React, { useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import type { ServicesStackParamList } from '../../../navigation/types';
import { ScreenShell, Card, Input, Button } from '../../../components';
import { useToast } from '../../../hooks/useToast';
import { useTheme } from '../../../theme';
type Props = NativeStackScreenProps<ServicesStackParamList, 'StopPayment'>;
export default function StopPaymentScreen({ navigation }: Props) {
const theme = useTheme();
const toast = useToast();
const [account, setAccount] = useState('0000XXXX1234');
const [chequeNumber, setChequeNumber] = useState('');
const [reason, setReason] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = () => {
if (!chequeNumber) {
toast('Cheque number required', 'error', 'Please enter a valid cheque number.');
return;
}
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
toast('Payment Stopped', 'success', `Cheque #${chequeNumber} has been blocked.`);
navigation.goBack();
}, 1500);
};
return (
<ScreenShell title="Stop Payment" subtitle="Block a specific cheque">
<Card variant="default" style={styles.card}>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Select Account</Text>
<Input value={account} onChangeText={setAccount} placeholder="Account Number" />
</View>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Cheque Number</Text>
<Input value={chequeNumber} onChangeText={setChequeNumber} placeholder="6-digit unique number" keyboardType="number-pad" />
</View>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Reason (Optional)</Text>
<Input value={reason} onChangeText={setReason} placeholder="e.g. Lost in transit" />
</View>
<Button
title="Confirm Stop Payment"
onPress={handleSubmit}
loading={isLoading}
style={styles.submitBtn}
variant="danger"
/>
</Card>
</ScreenShell>
);
}
const styles = StyleSheet.create({
card: { padding: 16, gap: 16 },
formGroup: { gap: 8 },
label: { fontSize: 13, fontWeight: '600' },
submitBtn: { marginTop: 16 },
});