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,157 @@
import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import Ionicons from '@react-native-vector-icons/ionicons';
import { Card } from '../../../components';
import { useTheme } from '../../../theme';
import { AwarenessArticle } from '../types';
interface AwarenessCardProps {
article: AwarenessArticle;
isBookmarked: boolean;
onToggleBookmark: () => void;
onPress: () => void;
}
export const AwarenessCard = ({
article,
isBookmarked,
onToggleBookmark,
onPress,
}: AwarenessCardProps) => {
const theme = useTheme();
return (
<Pressable onPress={onPress}>
<Card style={styles.card}>
<View style={styles.header}>
<View style={[styles.iconFrame, { backgroundColor: `${theme.colors.primary}12` }]}>
<Ionicons name={article.icon as any} size={22} color={theme.colors.primary} />
</View>
<View style={styles.headerInfo}>
<View style={styles.titleRow}>
<Text style={[styles.category, { color: theme.colors.primary }]}>
{article.category}
</Text>
{article.isUrgent && (
<View style={[styles.urgentBadge, { backgroundColor: `${theme.colors.error}15` }]}>
<Text style={[styles.urgentText, { color: theme.colors.error }]}>URGENT</Text>
</View>
)}
</View>
<Text style={[styles.title, { color: theme.colors.textPrimary }]} numberOfLines={1}>
{article.title}
</Text>
</View>
<Pressable hitSlop={10} onPress={onToggleBookmark} style={styles.bookmarkBtn}>
<Ionicons
name={isBookmarked ? 'bookmark' : 'bookmark-outline'}
size={22}
color={isBookmarked ? theme.colors.primary : theme.colors.textMuted}
/>
</Pressable>
</View>
<Text style={[styles.description, { color: theme.colors.textSecondary }]} numberOfLines={2}>
{article.description}
</Text>
<View style={styles.footer}>
<View style={styles.tags}>
{article.tags.slice(0, 2).map((tag) => (
<View key={tag} style={[styles.tag, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.tagText, { color: theme.colors.textSecondary }]}>#{tag}</Text>
</View>
))}
</View>
<View style={styles.readMore}>
<Text style={[styles.readMoreText, { color: theme.colors.primary }]}>Read More</Text>
<Ionicons name="chevron-forward" size={14} color={theme.colors.primary} />
</View>
</View>
</Card>
</Pressable>
);
};
const styles = StyleSheet.create({
card: {
padding: 16,
marginBottom: 16,
gap: 12,
},
header: {
flexDirection: 'row',
gap: 12,
alignItems: 'center',
},
iconFrame: {
width: 44,
height: 44,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
headerInfo: {
flex: 1,
gap: 2,
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
category: {
fontSize: 11,
fontWeight: '800',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
urgentBadge: {
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
},
urgentText: {
fontSize: 9,
fontWeight: '900',
},
title: {
fontSize: 16,
fontWeight: '700',
},
bookmarkBtn: {
padding: 4,
},
description: {
fontSize: 13,
lineHeight: 18,
},
footer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginTop: 4,
},
tags: {
flexDirection: 'row',
gap: 8,
},
tag: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 8,
},
tagText: {
fontSize: 10,
fontWeight: '600',
},
readMore: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
readMoreText: {
fontSize: 13,
fontWeight: '700',
},
});
@@ -0,0 +1,91 @@
import { AwarenessArticle } from '../types';
export const awarenessArticles: AwarenessArticle[] = [
{
id: 'art_001',
title: 'Recognizing UPI QR Code Scams',
description: 'Learn how scammers use fake QR codes to steal money from your account.',
content: `UPI QR codes are meant for making payments, not receiving them. Scammers often send QR codes to victims claiming they will receive money by scanning it.
Key Safety Tips:
1. Scanning a QR code always means money will be deducted from YOUR account.
2. You NEVER need to scan a QR code or enter your UPI PIN to RECEIVE money.
3. If someone asks you to scan a QR code to "collect prize money," it's a scam.
4. Always verify the merchant name displayed in the UPI app before confirming a payment.`,
category: 'NPCI',
tags: ['UPI', 'Scam'],
icon: 'qr-code-outline',
publishDate: '2026-04-10',
isUrgent: true,
},
{
id: 'art_002',
title: 'Protecting Your OTP',
description: 'Why you should never share your One Time Password with anyone.',
content: `One Time Passwords (OTPs) are the final barrier protecting your transactions. No bank official will ever ask for your OTP over the phone or via email.
Safe Practices:
- Never share OTP, CVV, or PIN with anyone, including bank staff.
- Ensure your mobile number is updated with the bank to receive alerts.
- Do not click on links in SMS that ask for your OTP.
- Use biometric authentication where available for added security.`,
category: 'RBI',
tags: ['OTP', 'Scam'],
icon: 'shield-checkmark-outline',
publishDate: '2026-04-08',
},
{
id: 'art_003',
title: 'Fake Call/Vishing Alerts',
description: 'How to handle calls from individuals pretending to be bank employees.',
content: `Vishing (voice phishing) is a common method where scammers call you pretending to be from your bank's KYC or security department.
How to identify Vishing:
1. Sense of Urgency: They claim your account will be blocked immediately.
2. Asking for Sensitive Info: They ask for PIN, OTP, or passwords.
3. Call from unknown mobile numbers: Banks usually call from official numbers.
What to do:
- Hang up immediately if you feel suspicious.
- Call your bank's official customer care number to verify.
- Report the number on the Cyber Crime portal.`,
category: 'Fraud Alert',
tags: ['Phishing', 'Scam'],
icon: 'call-outline',
publishDate: '2026-04-12',
isUrgent: true,
},
{
id: 'art_004',
title: 'Safe Online Banking Habits',
description: 'Essential practices for secure net banking and mobile app usage.',
content: `Securing your digital footprint is vital in modern banking. Follow these RBI-recommended practices.
Checklist:
- Use strong, unique passwords for banking apps.
- Enable Two-Factor Authentication (2FA).
- Avoid using public Wi-Fi for banking transactions.
- Always log out after your session.
- Keep your banking app updated to the latest version.`,
category: 'Best Practices',
tags: ['Net Banking', 'Scam'],
icon: 'globe-outline',
publishDate: '2026-04-05',
},
{
id: 'art_005',
title: 'NPCI: UPI Safety Shield',
description: 'NPCI’s official guide to safe digital payments.',
content: `NPCI encourages users to be "UPI Chalak" (UPI Smart).
Security Features:
- PIN Entry: Required only for sending money or checking balance.
- Spam Filter: UPI apps now highlight suspicious payment requests.
- Transaction Limits: Set daily limits for UPI transactions in your bank app.
- Block & Report: Use the "Report" feature for fraudulent IDs.`,
category: 'NPCI',
tags: ['UPI'],
icon: 'flash-outline',
publishDate: '2026-04-01',
},
];
@@ -0,0 +1,81 @@
import { useState, useCallback, useEffect, useMemo } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { awarenessArticles } from '../constants/mockData';
import { AwarenessArticle, AwarenessFilter } from '../types';
const BOOKMARKS_KEY = '@awareness_bookmarks';
export const useAwareness = () => {
const [articles] = useState<AwarenessArticle[]>(awarenessArticles);
const [bookmarks, setBookmarks] = useState<string[]>([]);
const [filter, setFilter] = useState<AwarenessFilter>({
category: 'All',
tag: 'All',
searchQuery: '',
});
const [isLoading, setIsLoading] = useState(true);
// Load bookmarks on mount
useEffect(() => {
const loadBookmarks = async () => {
try {
const saved = await AsyncStorage.getItem(BOOKMARKS_KEY);
if (saved) {
setBookmarks(JSON.parse(saved));
}
} catch (error) {
console.error('Failed to load bookmarks', error);
} finally {
setIsLoading(false);
}
};
loadBookmarks();
}, []);
// Save bookmarks when changed
const toggleBookmark = useCallback(async (id: string) => {
try {
const newBookmarks = bookmarks.includes(id)
? bookmarks.filter((b) => b !== id)
: [...bookmarks, id];
setBookmarks(newBookmarks);
await AsyncStorage.setItem(BOOKMARKS_KEY, JSON.stringify(newBookmarks));
} catch (error) {
console.error('Failed to update bookmarks', error);
}
}, [bookmarks]);
const filteredArticles = useMemo(() => {
return articles.filter((article) => {
const matchesCategory = filter.category === 'All' || article.category === filter.category;
const matchesTag = filter.tag === 'All' || article.tags.includes(filter.tag as any);
const matchesSearch =
article.title.toLowerCase().includes(filter.searchQuery.toLowerCase()) ||
article.description.toLowerCase().includes(filter.searchQuery.toLowerCase()) ||
article.content.toLowerCase().includes(filter.searchQuery.toLowerCase());
return matchesCategory && matchesTag && matchesSearch;
});
}, [articles, filter]);
const urgentArticles = useMemo(() => {
return articles.filter((a) => a.isUrgent);
}, [articles]);
const bookmarkedArticles = useMemo(() => {
return articles.filter((a) => bookmarks.includes(a.id));
}, [articles, bookmarks]);
return {
articles: filteredArticles,
urgentArticles,
bookmarkedArticles,
bookmarks,
toggleBookmark,
isLoading,
filter,
setFilter,
};
};
@@ -0,0 +1,246 @@
import React from 'react';
import { Share, StyleSheet, Text, View, Pressable } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import Ionicons from '@react-native-vector-icons/ionicons';
import { Card, ScreenShell } from '../../../components';
import { ServicesStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
import { useAwareness } from '../hooks/useAwareness';
type Props = NativeStackScreenProps<ServicesStackParamList, 'AwarenessDetail'>;
export default function AwarenessDetailScreen({ route, navigation }: Props) {
const { articleId } = route.params;
const theme = useTheme();
const { articles, bookmarks, toggleBookmark } = useAwareness();
const article = articles.find((a) => a.id === articleId);
const isBookmarked = bookmarks.includes(articleId);
if (!article) {
return (
<ScreenShell title="Not Found">
<View style={styles.errorContainer}>
<Text style={{ color: theme.colors.textPrimary }}>Article not found.</Text>
</View>
</ScreenShell>
);
}
const handleShare = async () => {
try {
await Share.share({
message: `${article.title}\n\n${article.description}\n\nStay safe with BankCBS!`,
title: article.title,
});
} catch (error) {
console.error('Error sharing article', error);
}
};
const rightContent = (
<View style={styles.headerActions}>
<Pressable onPress={handleShare} style={[styles.actionBtn, { backgroundColor: theme.colors.surfaceMuted }]}>
<Ionicons name="share-social-outline" size={20} color={theme.colors.textPrimary} />
</Pressable>
<Pressable
onPress={() => toggleBookmark(articleId)}
style={[styles.actionBtn, { backgroundColor: isBookmarked ? `${theme.colors.primary}15` : theme.colors.surfaceMuted }]}
>
<Ionicons
name={isBookmarked ? 'bookmark' : 'bookmark-outline'}
size={20}
color={isBookmarked ? theme.colors.primary : theme.colors.textPrimary}
/>
</Pressable>
</View>
);
return (
<ScreenShell
title="Article"
subtitle={article.category}
rightContent={rightContent}
>
<View style={styles.container}>
<View style={styles.metaRow}>
<View style={[styles.categoryBadge, { backgroundColor: `${theme.colors.primary}12` }]}>
<Text style={[styles.categoryText, { color: theme.colors.primary }]}>{article.category}</Text>
</View>
<Text style={[styles.dateText, { color: theme.colors.textMuted }]}>{article.publishDate}</Text>
</View>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>{article.title}</Text>
<View style={styles.tagsRow}>
{article.tags.map(tag => (
<View key={tag} style={[styles.tag, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.tagText, { color: theme.colors.textSecondary }]}>#{tag}</Text>
</View>
))}
</View>
<Card variant="glass" style={styles.introCard}>
<Text style={[styles.description, { color: theme.colors.textSecondary }]}>
{article.description}
</Text>
</Card>
<View style={styles.contentSection}>
<Text style={[styles.contentText, { color: theme.colors.textPrimary }]}>
{article.content}
</Text>
</View>
<View style={styles.safetyCta}>
<View style={[styles.ctaIcon, { backgroundColor: `${theme.colors.success}15` }]}>
<Ionicons name="shield-checkmark-outline" size={24} color={theme.colors.success} />
</View>
<View style={styles.ctaContent}>
<Text style={[styles.ctaTitle, { color: theme.colors.textPrimary }]}>Stay Protected</Text>
<Text style={[styles.ctaSubtitle, { color: theme.colors.textSecondary }]}>
BankCBS never asks for your OTP, PIN, or CVV. Report any suspicious activity immediately.
</Text>
</View>
</View>
<View style={styles.footerActions}>
<Pressable
onPress={() => navigation.navigate('ServiceRequestsHub')}
style={[styles.reportBtn, { borderColor: theme.colors.cardBorder }]}
>
<Ionicons name="megaphone-outline" size={18} color={theme.colors.error} />
<Text style={[styles.reportText, { color: theme.colors.error }]}>Report Fraudulent Activity</Text>
</Pressable>
</View>
</View>
<View style={{ height: 20 }} />
</ScreenShell>
);
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 16,
paddingBottom: 40,
},
headerActions: {
flexDirection: 'row',
gap: 8,
},
actionBtn: {
width: 40,
height: 40,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
metaRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
categoryBadge: {
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 8,
},
categoryText: {
fontSize: 11,
fontWeight: '800',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
dateText: {
fontSize: 12,
fontWeight: '600',
},
title: {
fontSize: 24,
fontWeight: '800',
letterSpacing: -0.5,
lineHeight: 30,
marginBottom: 12,
},
tagsRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginBottom: 20,
},
tag: {
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 10,
},
tagText: {
fontSize: 11,
fontWeight: '700',
},
introCard: {
padding: 16,
marginBottom: 24,
},
description: {
fontSize: 15,
lineHeight: 22,
fontWeight: '500',
},
contentSection: {
marginBottom: 32,
},
contentText: {
fontSize: 16,
lineHeight: 26,
},
safetyCta: {
flexDirection: 'row',
backgroundColor: 'rgba(52, 211, 153, 0.05)',
padding: 20,
borderRadius: 20,
gap: 16,
marginBottom: 32,
},
ctaIcon: {
width: 48,
height: 48,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
ctaContent: {
flex: 1,
gap: 4,
},
ctaTitle: {
fontSize: 16,
fontWeight: '800',
},
ctaSubtitle: {
fontSize: 13,
lineHeight: 18,
},
footerActions: {
gap: 12,
},
reportBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 10,
paddingVertical: 14,
borderRadius: 16,
borderWidth: 1,
borderStyle: 'dashed',
},
reportText: {
fontSize: 14,
fontWeight: '700',
},
errorContainer: {
padding: 20,
alignItems: 'center',
},
});
@@ -0,0 +1,215 @@
import React from 'react';
import { FlatList, ScrollView, StyleSheet, Text, View, Pressable } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import Ionicons from '@react-native-vector-icons/ionicons';
import { Input, ScreenShell } from '../../../components';
import { ServicesStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
import { AwarenessCard } from '../components/AwarenessCard';
import { useAwareness } from '../hooks/useAwareness';
import { AwarenessCategory } from '../types';
type Props = NativeStackScreenProps<ServicesStackParamList, 'AwarenessList'>;
const CATEGORIES: (AwarenessCategory | 'All')[] = ['All', 'RBI', 'NPCI', 'Fraud Alert', 'Best Practices'];
export default function AwarenessListScreen({ navigation }: Props) {
const theme = useTheme();
const { articles, filter, setFilter, bookmarks, toggleBookmark, urgentArticles } = useAwareness();
const handleSearch = (query: string) => {
setFilter({ ...filter, searchQuery: query });
};
const handleCategoryPress = (category: AwarenessCategory | 'All') => {
setFilter({ ...filter, category });
};
return (
<ScreenShell title="Security aware" subtitle="Learn to protect your digital banking">
<View style={styles.searchContainer}>
<Input
placeholder="Search safety tips, fraud alerts..."
value={filter.searchQuery}
onChangeText={handleSearch}
leftAccessory={<Ionicons name="search-outline" size={20} color={theme.colors.textMuted} />}
/>
</View>
<View style={styles.filterWrapper}>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filterContainer}>
{CATEGORIES.map((cat) => {
const isActive = filter.category === cat;
return (
<Pressable
key={cat}
onPress={() => handleCategoryPress(cat)}
style={[
styles.filterChip,
{
backgroundColor: isActive ? theme.colors.primary : theme.colors.surfaceMuted,
borderColor: isActive ? theme.colors.primary : theme.colors.cardBorder,
},
]}
>
<Text
style={[
styles.filterText,
{ color: isActive ? theme.colors.white : theme.colors.textSecondary },
]}
>
{cat}
</Text>
</Pressable>
);
})}
</ScrollView>
</View>
<FlatList
data={articles}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<AwarenessCard
article={item}
isBookmarked={bookmarks.includes(item.id)}
onToggleBookmark={() => toggleBookmark(item.id)}
onPress={() => navigation.navigate('AwarenessDetail', { articleId: item.id })}
/>
)}
ListHeaderComponent={
<>
{urgentArticles.length > 0 && filter.category === 'All' && filter.searchQuery === '' && (
<View style={styles.urgentSection}>
<View style={styles.sectionHeader}>
<Ionicons name="alert-circle" size={18} color={theme.colors.error} />
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Critical Security Alerts
</Text>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.urgentCarousel}>
{urgentArticles.map((article) => (
<Pressable
key={article.id}
onPress={() => navigation.navigate('AwarenessDetail', { articleId: article.id })}
style={[styles.urgentCard, { backgroundColor: `${theme.colors.error}08`, borderColor: `${theme.colors.error}20` }]}
>
<View style={[styles.urgentIcon, { backgroundColor: `${theme.colors.error}15` }]}>
<Ionicons name={article.icon as any} size={20} color={theme.colors.error} />
</View>
<Text style={[styles.urgentCardTitle, { color: theme.colors.textPrimary }]} numberOfLines={1}>
{article.title}
</Text>
<Text style={[styles.urgentCardDesc, { color: theme.colors.textSecondary }]} numberOfLines={1}>
{article.description}
</Text>
</Pressable>
))}
</ScrollView>
</View>
)}
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
{filter.category === 'All' ? 'Latest Awareness Articles' : `${filter.category} Guidelines`}
</Text>
</View>
</>
}
contentContainerStyle={styles.listContent}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Ionicons name="search-outline" size={48} color={theme.colors.textMuted} />
<Text style={[styles.emptyTitle, { color: theme.colors.textPrimary }]}>No results found</Text>
<Text style={[styles.emptySubtitle, { color: theme.colors.textSecondary }]}>
Try adjusting your search or filters to find what you're looking for.
</Text>
</View>
}
scrollEnabled={false} // Since it's inside ScreenShell's ScrollView
/>
</ScreenShell>
);
}
const styles = StyleSheet.create({
searchContainer: {
paddingHorizontal: 16,
marginBottom: 16,
},
filterWrapper: {
marginBottom: 20,
},
filterContainer: {
paddingHorizontal: 16,
gap: 10,
},
filterChip: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
borderWidth: 1,
},
filterText: {
fontSize: 13,
fontWeight: '700',
},
listContent: {
paddingHorizontal: 16,
paddingBottom: 100,
},
urgentSection: {
marginBottom: 24,
gap: 12,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: 12,
},
sectionTitle: {
fontSize: 16,
fontWeight: '800',
letterSpacing: -0.3,
},
urgentCarousel: {
gap: 12,
},
urgentCard: {
width: 260,
padding: 16,
borderRadius: 16,
borderWidth: 1,
gap: 8,
},
urgentIcon: {
width: 36,
height: 36,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
urgentCardTitle: {
fontSize: 15,
fontWeight: '700',
},
urgentCardDesc: {
fontSize: 12,
},
emptyContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
gap: 12,
},
emptyTitle: {
fontSize: 18,
fontWeight: '800',
},
emptySubtitle: {
fontSize: 14,
textAlign: 'center',
paddingHorizontal: 40,
},
});
+21
View File
@@ -0,0 +1,21 @@
export type AwarenessCategory = 'RBI' | 'NPCI' | 'Fraud Alert' | 'Best Practices';
export type AwarenessTag = 'UPI' | 'Cards' | 'Net Banking' | 'OTP' | 'Phishing' | 'Scam';
export interface AwarenessArticle {
id: string;
title: string;
description: string;
content: string;
category: AwarenessCategory;
tags: AwarenessTag[];
icon: string;
imageUrl?: string;
publishDate: string;
isUrgent?: boolean;
}
export type AwarenessFilter = {
category: AwarenessCategory | 'All';
tag: AwarenessTag | 'All';
searchQuery: string;
};