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,86 @@
import React, { memo } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import Ionicons from '@react-native-vector-icons/ionicons';
import { useTheme } from '../../../theme';
import type { InsuranceCategory } from '../types';
import { INSURANCE_CATEGORY_META } from '../types';
interface InsuranceCategoryChipProps {
category: InsuranceCategory;
selected: boolean;
onPress: () => void;
}
const InsuranceCategoryChipComponent = ({
category,
selected,
onPress,
}: InsuranceCategoryChipProps) => {
const theme = useTheme();
const meta = INSURANCE_CATEGORY_META[category];
return (
<Pressable onPress={onPress}>
<View
style={[
styles.chip,
{
borderColor: selected ? meta.accentColor : theme.colors.cardBorder,
backgroundColor: selected ? `${meta.accentColor}10` : theme.colors.surface,
},
]}
>
<View
style={[
styles.iconWrap,
{
backgroundColor: selected ? `${meta.accentColor}18` : theme.colors.surfaceMuted,
},
]}
>
<Ionicons
name={meta.icon as any}
size={18}
color={selected ? meta.accentColor : theme.colors.textSecondary}
/>
</View>
<Text
style={[
styles.label,
{
color: selected ? theme.colors.textPrimary : theme.colors.textSecondary,
},
]}
>
{category}
</Text>
</View>
</Pressable>
);
};
const styles = StyleSheet.create({
chip: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
borderWidth: 1,
borderRadius: 16,
paddingHorizontal: 12,
paddingVertical: 10,
},
iconWrap: {
width: 34,
height: 34,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
label: {
fontSize: 13,
fontWeight: '700',
},
});
export const InsuranceCategoryChip = memo(InsuranceCategoryChipComponent);
@@ -0,0 +1,78 @@
import React, { memo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Card } from '../../../components';
import { useTheme } from '../../../theme';
import { formatCurrency, formatDate } from '../../../utils/formatters';
import type { InsuranceClaim } from '../types';
import { InsuranceStatusPill } from './InsuranceStatusPill';
interface InsuranceClaimCardProps {
claim: InsuranceClaim;
}
const InsuranceClaimCardComponent = ({ claim }: InsuranceClaimCardProps) => {
const theme = useTheme();
return (
<Card style={styles.card}>
<View style={styles.header}>
<View style={styles.copy}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>{claim.reason}</Text>
<Text style={[styles.meta, { color: theme.colors.textSecondary }]}>
{claim.claimNumber} · {claim.policyName}
</Text>
</View>
<InsuranceStatusPill status={claim.status} />
</View>
<Text style={[styles.notes, { color: theme.colors.textMuted }]}>{claim.notes}</Text>
<View style={styles.metrics}>
<Text style={[styles.metricText, { color: theme.colors.textPrimary }]}>
{formatCurrency(claim.amount)}
</Text>
<Text style={[styles.metricText, { color: theme.colors.textSecondary }]}>
Event {formatDate(claim.eventDate)}
</Text>
<Text style={[styles.metricText, { color: theme.colors.textSecondary }]}>
Updated {formatDate(claim.updatedAt)}
</Text>
</View>
</Card>
);
};
const styles = StyleSheet.create({
card: {
gap: 12,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 12,
},
copy: {
flex: 1,
gap: 2,
},
title: {
fontSize: 15,
fontWeight: '800',
},
meta: {
fontSize: 12,
fontWeight: '600',
},
notes: {
fontSize: 12,
lineHeight: 18,
},
metrics: {
gap: 4,
},
metricText: {
fontSize: 12,
fontWeight: '600',
},
});
export const InsuranceClaimCard = memo(InsuranceClaimCardComponent);
@@ -0,0 +1,106 @@
import React, { memo } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import Ionicons from '@react-native-vector-icons/ionicons';
import { useTheme } from '../../../theme';
import { formatDate } from '../../../utils/formatters';
import type { InsuranceDocument } from '../types';
import { InsuranceStatusPill } from './InsuranceStatusPill';
interface InsuranceDocumentItemProps {
document: InsuranceDocument;
onView?: () => void;
onReupload?: () => void;
}
const InsuranceDocumentItemComponent = ({
document,
onView,
onReupload,
}: InsuranceDocumentItemProps) => {
const theme = useTheme();
return (
<View
style={[
styles.card,
{
borderColor: theme.colors.cardBorder,
backgroundColor: theme.colors.surface,
},
]}
>
<View style={[styles.iconWrap, { backgroundColor: theme.colors.surfaceMuted }]}>
<Ionicons name="document-text-outline" size={20} color={theme.colors.primary} />
</View>
<View style={styles.copy}>
<Text style={[styles.name, { color: theme.colors.textPrimary }]}>{document.name}</Text>
<Text style={[styles.meta, { color: theme.colors.textMuted }]}>
{document.type} · {formatDate(document.uploadedAt)}
</Text>
{document.rejectionReason ? (
<Text style={[styles.reason, { color: theme.colors.error }]}>
{document.rejectionReason}
</Text>
) : null}
</View>
<View style={styles.actions}>
<InsuranceStatusPill status={document.status} />
{onView ? (
<Pressable onPress={onView}>
<Text style={[styles.link, { color: theme.colors.primary }]}>View</Text>
</Pressable>
) : null}
{onReupload ? (
<Pressable onPress={onReupload}>
<Text style={[styles.link, { color: theme.colors.primary }]}>Re-upload</Text>
</Pressable>
) : null}
</View>
</View>
);
};
const styles = StyleSheet.create({
card: {
flexDirection: 'row',
alignItems: 'flex-start',
borderRadius: 16,
borderWidth: 1,
padding: 14,
gap: 12,
},
iconWrap: {
width: 40,
height: 40,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
copy: {
flex: 1,
gap: 3,
},
name: {
fontSize: 14,
fontWeight: '800',
},
meta: {
fontSize: 12,
fontWeight: '600',
},
reason: {
fontSize: 11,
lineHeight: 16,
},
actions: {
alignItems: 'flex-end',
gap: 6,
},
link: {
fontSize: 12,
fontWeight: '700',
},
});
export const InsuranceDocumentItem = memo(InsuranceDocumentItemComponent);
@@ -0,0 +1,78 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { AppModal, Button, Input } from '../../../components';
import { useTheme } from '../../../theme';
interface InsuranceOtpModalProps {
visible: boolean;
title: string;
description: string;
otp: string;
loading?: boolean;
error?: string;
mockCodeHint?: string;
onOtpChange: (value: string) => void;
onClose: () => void;
onConfirm: () => void;
}
export const InsuranceOtpModal = ({
visible,
title,
description,
otp,
loading = false,
error,
mockCodeHint,
onOtpChange,
onClose,
onConfirm,
}: InsuranceOtpModalProps) => {
const theme = useTheme();
return (
<AppModal visible={visible} title={title} description={description} onClose={onClose}>
<Input
label="OTP"
value={otp}
onChangeText={onOtpChange}
keyboardType="number-pad"
maxLength={6}
placeholder="Enter 6-digit OTP"
error={error}
/>
{mockCodeHint ? (
<View
style={[styles.hintCard, { backgroundColor: theme.colors.surfaceMuted }]}
>
<Text style={[styles.hintLabel, { color: theme.colors.textSecondary }]}>
Mock OTP
</Text>
<Text style={[styles.hintValue, { color: theme.colors.textPrimary }]}>
{mockCodeHint}
</Text>
</View>
) : null}
<Button title="Verify & Continue" onPress={onConfirm} loading={loading} />
</AppModal>
);
};
const styles = StyleSheet.create({
hintCard: {
borderRadius: 14,
padding: 12,
gap: 4,
},
hintLabel: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
hintValue: {
fontSize: 16,
fontWeight: '800',
},
});
@@ -0,0 +1,114 @@
import React, { memo } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import Ionicons from '@react-native-vector-icons/ionicons';
import { useTheme } from '../../../theme';
import type { InsurancePaymentMethod } from '../types';
interface InsurancePaymentMethodTileProps {
method: InsurancePaymentMethod;
selected: boolean;
onPress: () => void;
}
const METHOD_META: Record<
InsurancePaymentMethod,
{ icon: string; label: string; description: string }
> = {
UPI: {
icon: 'phone-portrait-outline',
label: 'UPI',
description: 'Google Pay, PhonePe, BHIM, and other UPI apps',
},
'Net Banking': {
icon: 'globe-outline',
label: 'Net Banking',
description: 'Pay directly from your linked bank account',
},
Card: {
icon: 'card-outline',
label: 'Card',
description: 'Credit and debit cards with 3DS verification',
},
};
const InsurancePaymentMethodTileComponent = ({
method,
selected,
onPress,
}: InsurancePaymentMethodTileProps) => {
const theme = useTheme();
const meta = METHOD_META[method];
return (
<Pressable onPress={onPress}>
<View
style={[
styles.tile,
{
borderColor: selected ? theme.colors.primary : theme.colors.cardBorder,
borderWidth: selected ? 2 : 1,
backgroundColor: selected ? `${theme.colors.primary}08` : theme.colors.surface,
},
]}
>
<View
style={[
styles.iconWrap,
{
backgroundColor: selected ? `${theme.colors.primary}14` : theme.colors.surfaceMuted,
},
]}
>
<Ionicons
name={meta.icon as any}
size={20}
color={selected ? theme.colors.primary : theme.colors.textSecondary}
/>
</View>
<View style={styles.copy}>
<Text style={[styles.label, { color: theme.colors.textPrimary }]}>{meta.label}</Text>
<Text style={[styles.description, { color: theme.colors.textMuted }]}>
{meta.description}
</Text>
</View>
<Ionicons
name={selected ? 'radio-button-on' : 'radio-button-off'}
size={20}
color={selected ? theme.colors.primary : theme.colors.textMuted}
/>
</View>
</Pressable>
);
};
const styles = StyleSheet.create({
tile: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
borderRadius: 16,
padding: 14,
},
iconWrap: {
width: 42,
height: 42,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
copy: {
flex: 1,
gap: 2,
},
label: {
fontSize: 14,
fontWeight: '800',
},
description: {
fontSize: 12,
lineHeight: 17,
},
});
export const InsurancePaymentMethodTile = memo(InsurancePaymentMethodTileComponent);
@@ -0,0 +1,230 @@
import React, { memo } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import Ionicons from '@react-native-vector-icons/ionicons';
import { Button, Card } from '../../../components';
import { useTheme } from '../../../theme';
import type { InsurancePlan } from '../types';
import { formatCompactCurrency, formatCurrency } from '../../../utils/formatters';
interface InsurancePlanCardProps {
plan: InsurancePlan;
selected?: boolean;
compared?: boolean;
onSelect?: () => void;
onCompare?: () => void;
}
const InsurancePlanCardComponent = ({
plan,
selected = false,
compared = false,
onSelect,
onCompare,
}: InsurancePlanCardProps) => {
const theme = useTheme();
return (
<Card
style={[
styles.card,
{
borderColor: selected ? theme.colors.primary : theme.colors.cardBorder,
borderWidth: selected ? 2 : 1,
},
]}
>
<View style={styles.header}>
<View style={styles.copy}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>{plan.name}</Text>
<Text style={[styles.subtitle, { color: theme.colors.textSecondary }]}>
{plan.insurerName}
</Text>
</View>
<View
style={[styles.ratioBadge, { backgroundColor: `${theme.colors.accent}14` }]}
>
<Text style={[styles.ratioValue, { color: theme.colors.accent }]}>
{plan.claimSettlementRatio}%
</Text>
<Text style={[styles.ratioLabel, { color: theme.colors.textSecondary }]}>
CSR
</Text>
</View>
</View>
<Text style={[styles.summary, { color: theme.colors.textMuted }]}>{plan.summary}</Text>
<View style={styles.metrics}>
<View style={[styles.metricCard, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.metricLabel, { color: theme.colors.textSecondary }]}>
Premium
</Text>
<Text style={[styles.metricValue, { color: theme.colors.textPrimary }]}>
{formatCurrency(plan.premium)}
</Text>
<Text style={[styles.metricCaption, { color: theme.colors.textMuted }]}>
{plan.premiumFrequency}
</Text>
</View>
<View style={[styles.metricCard, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.metricLabel, { color: theme.colors.textSecondary }]}>
Coverage
</Text>
<Text style={[styles.metricValue, { color: theme.colors.textPrimary }]}>
{formatCompactCurrency(plan.coverAmount)}
</Text>
<Text style={[styles.metricCaption, { color: theme.colors.textMuted }]}>
{plan.tenureYears} year tenure
</Text>
</View>
</View>
<View style={styles.listWrap}>
{plan.benefits.slice(0, 3).map((benefit) => (
<View key={benefit} style={styles.listRow}>
<Ionicons name="checkmark-circle" size={16} color={theme.colors.accent} />
<Text style={[styles.listText, { color: theme.colors.textPrimary }]}>{benefit}</Text>
</View>
))}
</View>
<View style={styles.actionRow}>
<Pressable
onPress={onCompare}
style={({ pressed }) => [
styles.compareButton,
{
backgroundColor: compared ? `${theme.colors.info}12` : theme.colors.surfaceMuted,
opacity: pressed ? 0.9 : 1,
},
]}
>
<Ionicons
name={compared ? 'checkbox' : 'square-outline'}
size={18}
color={compared ? theme.colors.info : theme.colors.textMuted}
/>
<Text
style={[
styles.compareLabel,
{ color: compared ? theme.colors.info : theme.colors.textSecondary },
]}
>
Compare
</Text>
</Pressable>
<Button
title={selected ? 'Selected' : 'Select Plan'}
onPress={onSelect}
variant={selected ? 'secondary' : 'primary'}
fullWidth={false}
style={styles.selectButton}
/>
</View>
</Card>
);
};
const styles = StyleSheet.create({
card: {
gap: 14,
},
header: {
flexDirection: 'row',
gap: 12,
},
copy: {
flex: 1,
gap: 2,
},
title: {
fontSize: 16,
fontWeight: '800',
},
subtitle: {
fontSize: 12,
fontWeight: '600',
},
ratioBadge: {
minWidth: 68,
borderRadius: 14,
paddingHorizontal: 10,
paddingVertical: 8,
alignItems: 'center',
justifyContent: 'center',
},
ratioValue: {
fontSize: 16,
fontWeight: '900',
},
ratioLabel: {
fontSize: 10,
fontWeight: '800',
letterSpacing: 0.5,
},
summary: {
fontSize: 13,
lineHeight: 18,
},
metrics: {
flexDirection: 'row',
gap: 10,
},
metricCard: {
flex: 1,
borderRadius: 14,
padding: 12,
gap: 4,
},
metricLabel: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
metricValue: {
fontSize: 18,
fontWeight: '900',
},
metricCaption: {
fontSize: 11,
fontWeight: '600',
},
listWrap: {
gap: 8,
},
listRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
listText: {
flex: 1,
fontSize: 12,
fontWeight: '600',
lineHeight: 17,
},
actionRow: {
flexDirection: 'row',
gap: 10,
alignItems: 'center',
},
compareButton: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
borderRadius: 12,
paddingHorizontal: 12,
paddingVertical: 12,
},
compareLabel: {
fontSize: 12,
fontWeight: '700',
},
selectButton: {
flex: 1,
},
});
export const InsurancePlanCard = memo(InsurancePlanCardComponent);
@@ -0,0 +1,123 @@
import React, { memo } 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 { formatCompactCurrency, formatCurrency, formatDate } from '../../../utils/formatters';
import type { InsurancePolicy } from '../types';
import { InsuranceStatusPill } from './InsuranceStatusPill';
interface InsurancePolicyCardProps {
policy: InsurancePolicy;
onPress?: () => void;
}
const InsurancePolicyCardComponent = ({ policy, onPress }: InsurancePolicyCardProps) => {
const theme = useTheme();
return (
<Pressable onPress={onPress}>
<Card style={styles.card}>
<View style={styles.header}>
<View style={styles.copy}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>
{policy.planName}
</Text>
<Text style={[styles.meta, { color: theme.colors.textSecondary }]}>
{policy.policyNumber} · {policy.category}
</Text>
</View>
<InsuranceStatusPill status={policy.status} />
</View>
<View style={styles.metrics}>
<View style={styles.metric}>
<Text style={[styles.metricLabel, { color: theme.colors.textMuted }]}>Coverage</Text>
<Text style={[styles.metricValue, { color: theme.colors.textPrimary }]}>
{formatCompactCurrency(policy.coverageAmount)}
</Text>
</View>
<View style={styles.metric}>
<Text style={[styles.metricLabel, { color: theme.colors.textMuted }]}>Premium</Text>
<Text style={[styles.metricValue, { color: theme.colors.textPrimary }]}>
{formatCurrency(policy.premiumAmount)}
</Text>
</View>
</View>
<View style={styles.footer}>
<View style={styles.footerItem}>
<Ionicons name="time-outline" size={16} color={theme.colors.primary} />
<Text style={[styles.footerText, { color: theme.colors.textSecondary }]}>
Due {formatDate(policy.nextPremiumDueDate)}
</Text>
</View>
<View style={styles.footerItem}>
<Ionicons name="document-text-outline" size={16} color={theme.colors.primary} />
<Text style={[styles.footerText, { color: theme.colors.textSecondary }]}>
{policy.documents.length} document{policy.documents.length === 1 ? '' : 's'}
</Text>
</View>
</View>
</Card>
</Pressable>
);
};
const styles = StyleSheet.create({
card: {
gap: 14,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 12,
},
copy: {
flex: 1,
gap: 2,
},
title: {
fontSize: 16,
fontWeight: '800',
},
meta: {
fontSize: 12,
fontWeight: '600',
},
metrics: {
flexDirection: 'row',
gap: 12,
},
metric: {
flex: 1,
},
metricLabel: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
metricValue: {
fontSize: 18,
fontWeight: '900',
marginTop: 4,
},
footer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 14,
},
footerItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
footerText: {
fontSize: 12,
fontWeight: '600',
},
});
export const InsurancePolicyCard = memo(InsurancePolicyCardComponent);
@@ -0,0 +1,146 @@
import React, { useMemo, useRef, useState } from 'react';
import {
PanResponder,
StyleSheet,
Text,
View,
type LayoutChangeEvent,
} from 'react-native';
import { useTheme } from '../../../theme';
interface InsuranceRangeSliderProps {
label: string;
min: number;
max: number;
step: number;
value: number;
suffix?: string;
formatter?: (value: number) => string;
onValueChange: (value: number) => void;
}
export const InsuranceRangeSlider = ({
label,
min,
max,
step,
value,
suffix,
formatter,
onValueChange,
}: InsuranceRangeSliderProps) => {
const theme = useTheme();
const [trackWidth, setTrackWidth] = useState(0);
const latestWidth = useRef(0);
const percentage = useMemo(() => {
if (max === min) {
return 0;
}
return (value - min) / (max - min);
}, [max, min, value]);
const updateFromPosition = (positionX: number) => {
if (!latestWidth.current) {
return;
}
const clamped = Math.max(0, Math.min(positionX, latestWidth.current));
const rawValue = min + (clamped / latestWidth.current) * (max - min);
const steppedValue = Math.round(rawValue / step) * step;
const nextValue = Math.max(min, Math.min(steppedValue, max));
onValueChange(nextValue);
};
const panResponder = useMemo(
() =>
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: (event) => updateFromPosition(event.nativeEvent.locationX),
onPanResponderMove: (event) => updateFromPosition(event.nativeEvent.locationX),
}),
[max, min, step],
);
const handleLayout = (event: LayoutChangeEvent) => {
latestWidth.current = event.nativeEvent.layout.width;
setTrackWidth(event.nativeEvent.layout.width);
};
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>{label}</Text>
<Text style={[styles.value, { color: theme.colors.textPrimary }]}>
{formatter ? formatter(value) : `${value}${suffix ?? ''}`}
</Text>
</View>
<View
style={[styles.track, { backgroundColor: theme.colors.surfaceMuted }]}
onLayout={handleLayout}
{...panResponder.panHandlers}
>
<View
style={[
styles.progress,
{
backgroundColor: theme.colors.primary,
width: trackWidth * percentage,
},
]}
/>
<View
style={[
styles.thumb,
{
backgroundColor: theme.colors.white,
borderColor: theme.colors.primary,
left: Math.max(trackWidth * percentage - 10, 0),
},
]}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
gap: 10,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 10,
},
label: {
fontSize: 12,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
value: {
fontSize: 14,
fontWeight: '800',
},
track: {
height: 8,
borderRadius: 999,
justifyContent: 'center',
},
progress: {
height: 8,
borderRadius: 999,
},
thumb: {
position: 'absolute',
width: 20,
height: 20,
borderRadius: 10,
borderWidth: 3,
top: -6,
},
});
@@ -0,0 +1,51 @@
import React, { memo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { useTheme } from '../../../theme';
interface InsuranceStatusPillProps {
status: string;
}
const STATUS_COLORS: Record<string, string> = {
Active: '#10B981',
'Renewal Due': '#F59E0B',
Expired: '#EF4444',
'Grace Period': '#F97316',
Submitted: '#3B82F6',
'Under Review': '#6366F1',
Approved: '#10B981',
Rejected: '#EF4444',
Settled: '#059669',
uploaded: '#3B82F6',
verified: '#10B981',
rejected: '#EF4444',
};
const InsuranceStatusPillComponent = ({ status }: InsuranceStatusPillProps) => {
const theme = useTheme();
const color = STATUS_COLORS[status] ?? theme.colors.primary;
return (
<View style={[styles.pill, { backgroundColor: `${color}16` }]}>
<Text style={[styles.label, { color }]}>{status}</Text>
</View>
);
};
const styles = StyleSheet.create({
pill: {
borderRadius: 999,
paddingHorizontal: 10,
paddingVertical: 6,
alignSelf: 'flex-start',
},
label: {
fontSize: 11,
fontWeight: '800',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
});
export const InsuranceStatusPill = memo(InsuranceStatusPillComponent);
@@ -0,0 +1,21 @@
import { useCallback, useState } from 'react';
import { useGetInsuranceDashboardQuery } from '../services/insuranceApi';
export const useInsuranceDashboard = () => {
const query = useGetInsuranceDashboardQuery();
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await query.refetch();
setRefreshing(false);
}, [query]);
return {
...query,
dashboard: query.data,
refreshing,
onRefresh,
};
};
@@ -0,0 +1,5 @@
import type { InsuranceCategory } from '../types';
import { useGetInsurancePlansQuery } from '../services/insuranceApi';
export const useInsurancePlans = (category?: InsuranceCategory) =>
useGetInsurancePlansQuery(category ? { category } : undefined);
@@ -0,0 +1,622 @@
import React, { useEffect, useMemo, useState } from 'react';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { useAppDispatch, useAppSelector } from '../../../app/hooks';
import { Button, Card, Input, ScreenShell } from '../../../components';
import { ToggleSwitch } from '../../cards/components/ToggleSwitch';
import { useToast } from '../../../hooks/useToast';
import type { InsuranceStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
import { formatCompactCurrency, formatCurrency } from '../../../utils/formatters';
import { InsuranceCategoryChip } from '../components/InsuranceCategoryChip';
import { InsuranceOtpModal } from '../components/InsuranceOtpModal';
import { InsurancePaymentMethodTile } from '../components/InsurancePaymentMethodTile';
import { InsurancePlanCard } from '../components/InsurancePlanCard';
import { useInsurancePlans } from '../hooks/useInsurancePlans';
import {
usePurchaseInsurancePolicyMutation,
useRequestInsuranceOtpMutation,
} from '../services/insuranceApi';
import {
selectInsuranceComparedPlanIds,
selectInsuranceDraft,
selectInsurancePurchaseStep,
} from '../store/insuranceSelectors';
import { insuranceActions } from '../store/insuranceSlice';
import { INSURANCE_CATEGORIES, INSURANCE_CATEGORY_META } from '../types';
import { validateInsuranceOtp, validateInsurancePurchaseDraft } from '../utils/insuranceValidation';
type Props = NativeStackScreenProps<InsuranceStackParamList, 'BuyInsurance'>;
const STEP_LABELS = [
{ id: 'details', label: 'Details' },
{ id: 'plans', label: 'Plans' },
{ id: 'payment', label: 'Payment' },
] as const;
export default function BuyInsuranceScreen({ navigation, route }: Props) {
const dispatch = useAppDispatch();
const theme = useTheme();
const toast = useToast();
const draft = useAppSelector(selectInsuranceDraft);
const comparedPlanIds = useAppSelector(selectInsuranceComparedPlanIds);
const purchaseStep = useAppSelector(selectInsurancePurchaseStep);
const { data: plans = [], isLoading } = useInsurancePlans(draft.category);
const [requestOtp, requestOtpState] = useRequestInsuranceOtpMutation();
const [purchasePolicy, purchasePolicyState] = usePurchaseInsurancePolicyMutation();
const [errors, setErrors] = useState<Record<string, string>>({});
const [otp, setOtp] = useState('');
const [otpVisible, setOtpVisible] = useState(false);
const [otpError, setOtpError] = useState<string | undefined>();
const [mockCodeHint, setMockCodeHint] = useState<string | undefined>();
useEffect(() => {
if (route.params?.category) {
dispatch(insuranceActions.setInsuranceCategory(route.params.category));
}
}, [dispatch, route.params?.category]);
const selectedPlan = useMemo(
() => plans.find((plan) => plan.id === draft.selectedPlanId) ?? null,
[draft.selectedPlanId, plans],
);
const handleContinueFromDetails = () => {
const validationErrors = validateInsurancePurchaseDraft(draft, draft.category);
setErrors(validationErrors);
if (Object.keys(validationErrors).length) {
return;
}
dispatch(insuranceActions.setInsurancePurchaseStep('plans'));
};
const handleContinueFromPlans = () => {
if (!draft.selectedPlanId) {
setErrors({ selectedPlanId: 'Select a plan to continue.' });
return;
}
setErrors({});
dispatch(insuranceActions.setInsurancePurchaseStep('payment'));
};
const handleRequestOtp = async () => {
const error = !draft.selectedPlanId ? 'Select a plan to continue.' : undefined;
if (error) {
setErrors({ selectedPlanId: error });
return;
}
const result = await requestOtp({ action: 'purchase' });
if ('data' in result && result.data) {
setMockCodeHint(result.data.mockCodeHint);
setOtpVisible(true);
setOtpError(undefined);
return;
}
toast('Unable to send OTP', 'error', 'Please try again.');
};
const handleConfirmPurchase = async () => {
const error = validateInsuranceOtp(otp);
if (error) {
setOtpError(error);
return;
}
const result = await purchasePolicy({ draft, otp });
if ('data' in result && result.data) {
setOtpVisible(false);
setOtp('');
setOtpError(undefined);
toast(
'Policy issued',
'success',
`${result.data.policy.planName} is now active under ${result.data.policy.policyNumber}.`,
);
dispatch(insuranceActions.resetInsuranceDraft());
navigation.replace('InsurancePolicyDetails', { policyId: result.data.policy.id });
return;
}
setOtpError('Verification failed. Please try again.');
};
const renderStepPills = () => (
<View style={styles.stepRow}>
{STEP_LABELS.map((step) => {
const isActive = purchaseStep === step.id;
return (
<View
key={step.id}
style={[
styles.stepPill,
{
backgroundColor: isActive ? `${theme.colors.primary}12` : theme.colors.surfaceMuted,
},
]}
>
<Text
style={[
styles.stepLabel,
{ color: isActive ? theme.colors.primary : theme.colors.textSecondary },
]}
>
{step.label}
</Text>
</View>
);
})}
</View>
);
return (
<ScreenShell title="Buy Insurance" subtitle="Dynamic policy purchase flow with secure confirmation" scrollable={false}>
<ScrollView contentContainerStyle={styles.scroll} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled">
{renderStepPills()}
<Card style={styles.sectionCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Choose category
</Text>
<View style={styles.categoryWrap}>
{INSURANCE_CATEGORIES.map((category) => (
<InsuranceCategoryChip
key={category}
category={category}
selected={draft.category === category}
onPress={() => {
dispatch(insuranceActions.setInsuranceCategory(category));
setErrors({});
}}
/>
))}
</View>
<Text style={[styles.categoryCopy, { color: theme.colors.textSecondary }]}>
{INSURANCE_CATEGORY_META[draft.category].description}
</Text>
</Card>
{purchaseStep === 'details' ? (
<>
<Card style={styles.sectionCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Personal details
</Text>
<Input
label="Full Name"
value={draft.personal.fullName}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsurancePersonal({ field: 'fullName', value }),
)
}
error={errors.personalFullName}
/>
<View style={styles.inputRow}>
<View style={styles.inputCol}>
<Input
label="Age"
keyboardType="number-pad"
value={draft.personal.age}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsurancePersonal({ field: 'age', value }),
)
}
error={errors.personalAge}
/>
</View>
<View style={styles.inputCol}>
<Input
label="Gender"
value={draft.personal.gender}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsurancePersonal({ field: 'gender', value }),
)
}
error={errors.personalGender}
/>
</View>
</View>
<Input
label="Email"
keyboardType="email-address"
value={draft.personal.email}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsurancePersonal({ field: 'email', value }),
)
}
error={errors.personalEmail}
/>
<Input
label="Mobile Number"
keyboardType="number-pad"
value={draft.personal.phone}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsurancePersonal({ field: 'phone', value }),
)
}
error={errors.personalPhone}
/>
</Card>
<Card style={styles.sectionCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Nominee and address
</Text>
<Input
label="Nominee Name"
value={draft.nominee.name}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsuranceNominee({ field: 'name', value }),
)
}
error={errors.nomineeName}
/>
<View style={styles.inputRow}>
<View style={styles.inputCol}>
<Input
label="Relationship"
value={draft.nominee.relationship}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsuranceNominee({
field: 'relationship',
value,
}),
)
}
error={errors.nomineeRelationship}
/>
</View>
<View style={styles.inputCol}>
<Input
label="Nominee Age"
keyboardType="number-pad"
value={draft.nominee.age}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsuranceNominee({ field: 'age', value }),
)
}
error={errors.nomineeAge}
/>
</View>
</View>
<Input
label="Address"
value={draft.address.line1}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsuranceAddress({ field: 'line1', value }),
)
}
error={errors.addressLine1}
/>
<View style={styles.inputRow}>
<View style={styles.inputCol}>
<Input
label="City"
value={draft.address.city}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsuranceAddress({ field: 'city', value }),
)
}
error={errors.addressCity}
/>
</View>
<View style={styles.inputCol}>
<Input
label="State"
value={draft.address.state}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsuranceAddress({ field: 'state', value }),
)
}
error={errors.addressState}
/>
</View>
</View>
<Input
label="PIN Code"
keyboardType="number-pad"
value={draft.address.postalCode}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsuranceAddress({
field: 'postalCode',
value,
}),
)
}
error={errors.addressPostalCode}
/>
</Card>
<Card style={styles.sectionCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
KYC and policy-specific details
</Text>
<Input
label="Aadhaar Number"
keyboardType="number-pad"
value={draft.kyc.aadhaarNumber}
onChangeText={(value) =>
dispatch(insuranceActions.updateInsuranceKyc({ field: 'aadhaarNumber', value }))
}
error={errors.kycAadhaar}
/>
<Input
label="PAN Number"
value={draft.kyc.panNumber}
onChangeText={(value) =>
dispatch(insuranceActions.updateInsuranceKyc({ field: 'panNumber', value }))
}
error={errors.kycPan}
/>
{INSURANCE_CATEGORY_META[draft.category].specificFields.map((field) => (
<Input
key={field.key}
label={field.label}
value={draft.specificDetails[field.key] ?? ''}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsuranceSpecificDetail({
key: field.key,
value,
}),
)
}
keyboardType={field.keyboardType}
placeholder={field.placeholder}
multiline={field.multiline}
error={errors[`specific_${field.key}`]}
/>
))}
</Card>
<Button title="Continue to Plans" onPress={handleContinueFromDetails} />
</>
) : null}
{purchaseStep === 'plans' ? (
<>
<Card style={styles.sectionCard}>
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Available plans
</Text>
{comparedPlanIds.length >= 2 ? (
<Button
title="Compare"
variant="secondary"
fullWidth={false}
onPress={() =>
navigation.navigate('InsuranceComparison', {
planIds: comparedPlanIds,
category: draft.category,
})
}
/>
) : null}
</View>
{isLoading ? (
<Text style={[styles.helperText, { color: theme.colors.textSecondary }]}>
Loading plans...
</Text>
) : (
plans.map((plan) => (
<InsurancePlanCard
key={plan.id}
plan={plan}
selected={draft.selectedPlanId === plan.id}
compared={comparedPlanIds.includes(plan.id)}
onSelect={() => dispatch(insuranceActions.setInsuranceSelectedPlan(plan.id))}
onCompare={() =>
dispatch(insuranceActions.toggleInsuranceComparedPlan(plan.id))
}
/>
))
)}
{errors.selectedPlanId ? (
<Text style={[styles.errorText, { color: theme.colors.error }]}>
{errors.selectedPlanId}
</Text>
) : null}
</Card>
<View style={styles.actionStack}>
<Button
title="Back to Details"
variant="secondary"
onPress={() => dispatch(insuranceActions.setInsurancePurchaseStep('details'))}
/>
<Button title="Continue to Payment" onPress={handleContinueFromPlans} />
</View>
</>
) : null}
{purchaseStep === 'payment' ? (
<>
<Card style={styles.sectionCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Selected plan
</Text>
{selectedPlan ? (
<>
<View
style={[styles.summaryCard, { backgroundColor: theme.colors.surfaceMuted }]}
>
<Text style={[styles.summaryTitle, { color: theme.colors.textPrimary }]}>
{selectedPlan.name}
</Text>
<Text style={[styles.summaryCopy, { color: theme.colors.textSecondary }]}>
{selectedPlan.insurerName}
</Text>
<Text style={[styles.summaryAmount, { color: theme.colors.textPrimary }]}>
{formatCurrency(selectedPlan.premium)} · Coverage{' '}
{formatCompactCurrency(selectedPlan.coverAmount)}
</Text>
</View>
<InsurancePaymentMethodTile
method="UPI"
selected={draft.paymentMethod === 'UPI'}
onPress={() => dispatch(insuranceActions.setInsurancePaymentMethod('UPI'))}
/>
<InsurancePaymentMethodTile
method="Net Banking"
selected={draft.paymentMethod === 'Net Banking'}
onPress={() =>
dispatch(insuranceActions.setInsurancePaymentMethod('Net Banking'))
}
/>
<InsurancePaymentMethodTile
method="Card"
selected={draft.paymentMethod === 'Card'}
onPress={() => dispatch(insuranceActions.setInsurancePaymentMethod('Card'))}
/>
<ToggleSwitch
label="Enable auto-renew"
description="Keep this policy active by auto-paying the premium on the due date."
value={draft.autoRenewEnabled}
onValueChange={(value) => dispatch(insuranceActions.setInsuranceAutoRenew(value))}
/>
</>
) : (
<Text style={[styles.helperText, { color: theme.colors.textSecondary }]}>
No plan selected.
</Text>
)}
</Card>
<View style={styles.actionStack}>
<Button
title="Back to Plans"
variant="secondary"
onPress={() => dispatch(insuranceActions.setInsurancePurchaseStep('plans'))}
/>
<Button
title="Request OTP & Buy"
onPress={() => void handleRequestOtp()}
loading={requestOtpState.isLoading}
/>
</View>
</>
) : null}
</ScrollView>
<InsuranceOtpModal
visible={otpVisible}
title="Verify Policy Purchase"
description="Enter the OTP sent to your registered mobile number to authorize this insurance purchase."
otp={otp}
onOtpChange={setOtp}
onClose={() => setOtpVisible(false)}
onConfirm={() => void handleConfirmPurchase()}
loading={purchasePolicyState.isLoading}
error={otpError}
mockCodeHint={mockCodeHint}
/>
<View style={{ height: 20 }} />
</ScreenShell>
);
}
const styles = StyleSheet.create({
keyboardAvoid: {
flex: 1,
},
scroll: {
gap: 16,
paddingBottom: 28,
},
stepRow: {
flexDirection: 'row',
gap: 10,
},
stepPill: {
flex: 1,
borderRadius: 999,
alignItems: 'center',
paddingVertical: 10,
},
stepLabel: {
fontSize: 11,
fontWeight: '800',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
sectionCard: {
gap: 12,
},
sectionTitle: {
fontSize: 16,
fontWeight: '800',
},
categoryWrap: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 10,
},
categoryCopy: {
fontSize: 12,
lineHeight: 18,
},
inputRow: {
flexDirection: 'row',
gap: 12,
},
inputCol: {
flex: 1,
},
helperText: {
fontSize: 12,
lineHeight: 18,
},
errorText: {
fontSize: 12,
fontWeight: '600',
},
actionStack: {
gap: 10,
},
summaryCard: {
borderRadius: 14,
padding: 14,
gap: 4,
},
summaryTitle: {
fontSize: 15,
fontWeight: '800',
},
summaryCopy: {
fontSize: 12,
fontWeight: '600',
},
summaryAmount: {
fontSize: 13,
fontWeight: '700',
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12,
},
});
@@ -0,0 +1,248 @@
import React, { useEffect, useMemo } from 'react';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { useAppDispatch, useAppSelector } from '../../../app/hooks';
import { Button, Card, Input, ScreenShell } from '../../../components';
import type { InsuranceStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
import { formatCompactCurrency, formatCurrency } from '../../../utils/formatters';
import { InsuranceCategoryChip } from '../components/InsuranceCategoryChip';
import { InsuranceRangeSlider } from '../components/InsuranceRangeSlider';
import { useEstimateInsurancePremiumQuery } from '../services/insuranceApi';
import { insuranceActions } from '../store/insuranceSlice';
import { selectInsuranceCalculator } from '../store/insuranceSelectors';
import { INSURANCE_CATEGORIES } from '../types';
type Props = NativeStackScreenProps<InsuranceStackParamList, 'InsuranceCalculator'>;
export default function InsuranceCalculatorScreen({ navigation, route }: Props) {
const dispatch = useAppDispatch();
const theme = useTheme();
const calculator = useAppSelector(selectInsuranceCalculator);
const effectiveCategory = calculator.category;
useEffect(() => {
if (route.params?.category) {
dispatch(
insuranceActions.updateInsuranceCalculator({
field: 'category',
value: route.params.category,
}),
);
}
}, [dispatch, route.params?.category]);
const estimateQuery = useEstimateInsurancePremiumQuery({
...calculator,
category: effectiveCategory,
});
const estimate = estimateQuery.data;
const coverageValue = useMemo(
() => Number(calculator.coverageAmount || 0),
[calculator.coverageAmount],
);
return (
<ScreenShell title="Calculator" subtitle="Estimate premiums by age, cover, and tenure">
<ScrollView contentContainerStyle={styles.scroll} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled">
<Card style={styles.sectionCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Policy category
</Text>
<View style={styles.categoryWrap}>
{INSURANCE_CATEGORIES.map((category) => (
<InsuranceCategoryChip
key={category}
category={category}
selected={effectiveCategory === category}
onPress={() =>
dispatch(
insuranceActions.updateInsuranceCalculator({
field: 'category',
value: category,
}),
)
}
/>
))}
</View>
</Card>
<Card style={styles.sectionCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Adjust your assumptions
</Text>
<InsuranceRangeSlider
label="Age"
min={18}
max={70}
step={1}
value={Number(calculator.age || 18)}
onValueChange={(value) =>
dispatch(
insuranceActions.updateInsuranceCalculator({
field: 'age',
value: String(value),
}),
)
}
/>
<InsuranceRangeSlider
label="Coverage"
min={100000}
max={20000000}
step={100000}
value={coverageValue || 100000}
formatter={(value) => formatCompactCurrency(value)}
onValueChange={(value) =>
dispatch(
insuranceActions.updateInsuranceCalculator({
field: 'coverageAmount',
value: String(value),
}),
)
}
/>
<InsuranceRangeSlider
label="Tenure"
min={1}
max={30}
step={1}
value={Number(calculator.tenureYears || 1)}
suffix=" yr"
onValueChange={(value) =>
dispatch(
insuranceActions.updateInsuranceCalculator({
field: 'tenureYears',
value: String(value),
}),
)
}
/>
</Card>
<Card style={styles.sectionCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Manual inputs
</Text>
<Input
label="Age"
keyboardType="number-pad"
value={calculator.age}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsuranceCalculator({ field: 'age', value }),
)
}
/>
<Input
label="Coverage Amount"
keyboardType="number-pad"
value={calculator.coverageAmount}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsuranceCalculator({
field: 'coverageAmount',
value,
}),
)
}
/>
<Input
label="Tenure Years"
keyboardType="number-pad"
value={calculator.tenureYears}
onChangeText={(value) =>
dispatch(
insuranceActions.updateInsuranceCalculator({
field: 'tenureYears',
value,
}),
)
}
/>
</Card>
<Card style={styles.resultCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Estimated premium
</Text>
<View style={styles.resultGrid}>
<View style={[styles.resultMetric, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.resultLabel, { color: theme.colors.textSecondary }]}>
Monthly
</Text>
<Text style={[styles.resultValue, { color: theme.colors.textPrimary }]}>
{estimate ? formatCurrency(estimate.monthlyPremium) : '--'}
</Text>
</View>
<View style={[styles.resultMetric, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.resultLabel, { color: theme.colors.textSecondary }]}>
Annual
</Text>
<Text style={[styles.resultValue, { color: theme.colors.textPrimary }]}>
{estimate ? formatCurrency(estimate.annualPremium) : '--'}
</Text>
</View>
</View>
<Text style={[styles.recommendation, { color: theme.colors.textSecondary }]}>
Recommended protection: {estimate ? formatCompactCurrency(estimate.recommendedCover) : '--'}
</Text>
<Button
title="Buy This Cover"
onPress={() =>
navigation.navigate('BuyInsurance', { category: effectiveCategory })
}
/>
</Card>
</ScrollView>
</ScreenShell>
);
}
const styles = StyleSheet.create({
scroll: {
gap: 16,
paddingBottom: 28,
},
sectionCard: {
gap: 14,
},
sectionTitle: {
fontSize: 16,
fontWeight: '800',
},
categoryWrap: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 10,
},
resultCard: {
gap: 14,
},
resultGrid: {
flexDirection: 'row',
gap: 12,
},
resultMetric: {
flex: 1,
borderRadius: 16,
padding: 14,
gap: 4,
},
resultLabel: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
resultValue: {
fontSize: 18,
fontWeight: '900',
},
recommendation: {
fontSize: 12,
lineHeight: 18,
},
});
@@ -0,0 +1,392 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
FlatList,
RefreshControl,
StyleSheet,
Text,
View,
} from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { Button, Card, EmptyState, Input, Loader, ScreenShell } from '../../../components';
import { useToast } from '../../../hooks/useToast';
import type { InsuranceStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
import { formatCurrency, formatDate } from '../../../utils/formatters';
import { InsuranceClaimCard } from '../components/InsuranceClaimCard';
import { InsuranceDocumentItem } from '../components/InsuranceDocumentItem';
import { InsuranceOtpModal } from '../components/InsuranceOtpModal';
import {
useGetInsuranceClaimsQuery,
useGetInsurancePoliciesQuery,
useRequestInsuranceOtpMutation,
useSubmitInsuranceClaimMutation,
useUploadInsuranceDocumentMutation,
} from '../services/insuranceApi';
import type { InsuranceClaimDraft } from '../types';
import { validateInsuranceClaimDraft, validateInsuranceOtp } from '../utils/insuranceValidation';
import type { InsuranceDocument } from '../types';
type Props = NativeStackScreenProps<InsuranceStackParamList, 'InsuranceClaims'>;
const initialDraft: InsuranceClaimDraft = {
policyId: '',
reason: '',
eventDate: '',
amount: '',
description: '',
documentIds: [],
};
const SUPPORTING_DOCUMENTS = [
{
name: 'Hospital Invoice',
type: 'Invoice' as const,
category: 'claim' as const,
mimeType: 'application/pdf',
uri: 'https://mock.bankcbs.dev/uploads/hospital_invoice.pdf',
sizeKb: 520,
},
{
name: 'Medical Report',
type: 'Medical Report' as const,
category: 'claim' as const,
mimeType: 'application/pdf',
uri: 'https://mock.bankcbs.dev/uploads/medical_report.pdf',
sizeKb: 440,
},
{
name: 'Repair Invoice',
type: 'Invoice' as const,
category: 'claim' as const,
mimeType: 'application/pdf',
uri: 'https://mock.bankcbs.dev/uploads/repair_invoice.pdf',
sizeKb: 468,
},
];
export default function InsuranceClaimsScreen({ route }: Props) {
const theme = useTheme();
const toast = useToast();
const claimsQuery = useGetInsuranceClaimsQuery(
route.params?.policyId ? { policyId: route.params.policyId } : undefined,
);
const policiesQuery = useGetInsurancePoliciesQuery();
const [uploadDocument, uploadState] = useUploadInsuranceDocumentMutation();
const [requestOtp, requestOtpState] = useRequestInsuranceOtpMutation();
const [submitClaim, submitClaimState] = useSubmitInsuranceClaimMutation();
const [draft, setDraft] = useState<InsuranceClaimDraft>({
...initialDraft,
policyId: route.params?.policyId ?? '',
});
const [attachedDocuments, setAttachedDocuments] = useState<InsuranceDocument[]>([]);
const [errors, setErrors] = useState<Record<string, string>>({});
const [otp, setOtp] = useState('');
const [otpVisible, setOtpVisible] = useState(false);
const [otpError, setOtpError] = useState<string | undefined>();
const [mockCodeHint, setMockCodeHint] = useState<string | undefined>();
useEffect(() => {
if (route.params?.policyId) {
setDraft((current) => ({ ...current, policyId: route.params?.policyId ?? '' }));
}
}, [route.params?.policyId]);
const selectedPolicy = useMemo(
() => (policiesQuery.data ?? []).find((policy) => policy.id === draft.policyId) ?? null,
[draft.policyId, policiesQuery.data],
);
const handleUpload = async (index: number) => {
const sample = SUPPORTING_DOCUMENTS[index];
if (!sample) {
return;
}
const result = await uploadDocument(sample);
if ('data' in result && result.data) {
setDraft((current) => ({
...current,
documentIds: [...current.documentIds, result.data.id],
}));
setAttachedDocuments((current) => [result.data, ...current]);
toast('Document attached', 'success', `${sample.name} added to this claim.`);
return;
}
toast('Unable to attach document', 'error', 'Please try again.');
};
const handleRequestOtp = async () => {
const validationErrors = validateInsuranceClaimDraft(draft);
setErrors(validationErrors);
if (Object.keys(validationErrors).length) {
return;
}
const result = await requestOtp({ action: 'claim', policyId: draft.policyId });
if ('data' in result && result.data) {
setMockCodeHint(result.data.mockCodeHint);
setOtpVisible(true);
setOtpError(undefined);
return;
}
toast('Unable to send OTP', 'error', 'Please try again.');
};
const handleSubmitClaim = async () => {
const error = validateInsuranceOtp(otp);
if (error) {
setOtpError(error);
return;
}
const result = await submitClaim({
policyId: draft.policyId,
reason: draft.reason,
eventDate: draft.eventDate,
amount: Number(draft.amount),
description: draft.description,
documentIds: draft.documentIds,
otp,
});
if ('data' in result && result.data) {
setOtpVisible(false);
setOtp('');
setDraft({
...initialDraft,
policyId: route.params?.policyId ?? '',
});
setAttachedDocuments([]);
setErrors({});
toast(
'Claim submitted',
'success',
`${result.data.referenceId} has been filed successfully.`,
);
return;
}
setOtpError('Claim submission failed. Please try again.');
};
return (
<ScreenShell title="Claims" subtitle="File new claims and track history" scrollable={false}>
<FlatList
data={claimsQuery.data ?? []}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={claimsQuery.isFetching || policiesQuery.isFetching}
onRefresh={() => {
void claimsQuery.refetch();
void policiesQuery.refetch();
}}
tintColor={theme.colors.primary}
colors={[theme.colors.primary]}
/>
}
ListHeaderComponent={
<View style={styles.headerWrap}>
<Card style={styles.formCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
File a new claim
</Text>
<View style={styles.policyWrap}>
{(policiesQuery.data ?? []).map((policy) => (
<Button
key={policy.id}
title={policy.planName}
variant={draft.policyId === policy.id ? 'primary' : 'secondary'}
onPress={() => setDraft((current) => ({ ...current, policyId: policy.id }))}
/>
))}
</View>
{errors.policyId ? (
<Text style={[styles.errorText, { color: theme.colors.error }]}>
{errors.policyId}
</Text>
) : null}
<Input
label="Claim Reason"
value={draft.reason}
onChangeText={(value) => setDraft((current) => ({ ...current, reason: value }))}
error={errors.reason}
placeholder="Hospitalization, accident, repair, trip delay, etc."
/>
<Input
label="Event Date"
value={draft.eventDate}
onChangeText={(value) => setDraft((current) => ({ ...current, eventDate: value }))}
error={errors.eventDate}
placeholder="YYYY-MM-DD"
/>
<Input
label="Claim Amount"
value={draft.amount}
keyboardType="number-pad"
onChangeText={(value) => setDraft((current) => ({ ...current, amount: value }))}
error={errors.amount}
placeholder="Enter claim amount"
/>
<Input
label="Claim Details"
value={draft.description}
onChangeText={(value) =>
setDraft((current) => ({ ...current, description: value }))
}
error={errors.description}
placeholder="Describe the event and supporting details"
multiline
/>
{selectedPolicy ? (
<View
style={[styles.policySummary, { backgroundColor: theme.colors.surfaceMuted }]}
>
<Text style={[styles.policySummaryTitle, { color: theme.colors.textPrimary }]}>
{selectedPolicy.planName}
</Text>
<Text style={[styles.policySummaryText, { color: theme.colors.textSecondary }]}>
{selectedPolicy.policyNumber} · Coverage {formatCurrency(selectedPolicy.coverageAmount)}
</Text>
</View>
) : null}
<View style={styles.attachRow}>
<View style={styles.attachCol}>
<Button
title="Attach Invoice"
variant="secondary"
onPress={() => void handleUpload(0)}
loading={uploadState.isLoading}
/>
</View>
<View style={styles.attachCol}>
<Button
title="Attach Report"
variant="secondary"
onPress={() => void handleUpload(1)}
loading={uploadState.isLoading}
/>
</View>
</View>
<Button
title="Attach Repair PDF"
variant="secondary"
onPress={() => void handleUpload(2)}
loading={uploadState.isLoading}
/>
{errors.documents ? (
<Text style={[styles.errorText, { color: theme.colors.error }]}>
{errors.documents}
</Text>
) : null}
{attachedDocuments.map((document) => (
<InsuranceDocumentItem key={document.id} document={document} />
))}
<Button
title="Request OTP & Submit"
onPress={() => void handleRequestOtp()}
loading={requestOtpState.isLoading}
/>
</Card>
</View>
}
renderItem={({ item }) => <InsuranceClaimCard claim={item} />}
ItemSeparatorComponent={() => <View style={styles.separator} />}
ListEmptyComponent={
claimsQuery.isLoading ? (
<Card>
<Loader />
</Card>
) : claimsQuery.isError ? (
<EmptyState
title="Unable to load claims"
description="Refresh and try again."
actionLabel="Retry"
onAction={() => void claimsQuery.refetch()}
/>
) : (
<EmptyState
title="No claims found"
description="Submitted claims and settlement updates will appear here."
/>
)
}
/>
<InsuranceOtpModal
visible={otpVisible}
title="Verify Claim Submission"
description="Enter the OTP sent to your registered mobile number to authorize this claim request."
otp={otp}
onOtpChange={setOtp}
onClose={() => setOtpVisible(false)}
onConfirm={() => void handleSubmitClaim()}
loading={submitClaimState.isLoading}
error={otpError}
mockCodeHint={mockCodeHint}
/>
</ScreenShell>
);
}
const styles = StyleSheet.create({
listContent: {
paddingBottom: 70,
},
headerWrap: {
marginBottom: 16,
},
formCard: {
gap: 12,
},
sectionTitle: {
fontSize: 16,
fontWeight: '800',
},
policyWrap: {
gap: 10,
},
policySummary: {
borderRadius: 14,
padding: 12,
gap: 4,
},
policySummaryTitle: {
fontSize: 14,
fontWeight: '800',
},
policySummaryText: {
fontSize: 12,
lineHeight: 17,
},
attachRow: {
flexDirection: 'row',
gap: 12,
},
attachCol: {
flex: 1,
},
errorText: {
fontSize: 12,
fontWeight: '600',
},
separator: {
height: 12,
},
});
@@ -0,0 +1,162 @@
import React from 'react';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { Button, Card, EmptyState, Loader, ScreenShell } from '../../../components';
import type { InsuranceStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
import { formatCompactCurrency, formatCurrency } from '../../../utils/formatters';
import { useCompareInsurancePlansQuery } from '../services/insuranceApi';
type Props = NativeStackScreenProps<InsuranceStackParamList, 'InsuranceComparison'>;
const comparisonRows = [
{ key: 'premium', label: 'Premium' },
{ key: 'coverage', label: 'Coverage' },
{ key: 'csr', label: 'Claim ratio' },
{ key: 'benefits', label: 'Benefits' },
] as const;
export default function InsuranceComparisonScreen({ navigation, route }: Props) {
const theme = useTheme();
const { data, isLoading, isError, refetch } = useCompareInsurancePlansQuery(route.params.planIds);
return (
<ScreenShell title="Comparison" subtitle="Side-by-side plan evaluation">
{isLoading ? (
<Card>
<Loader />
</Card>
) : null}
{isError ? (
<EmptyState
title="Unable to compare plans"
description="Refresh and try again."
actionLabel="Retry"
onAction={refetch}
/>
) : null}
{!isLoading && !isError && (!data || data.length < 2) ? (
<EmptyState
title="Select at least two plans"
description="Add plans to the comparison tray from the buy insurance journey."
actionLabel="Back to Buy Flow"
onAction={() => navigation.navigate('BuyInsurance', { category: route.params.category })}
/>
) : null}
{data && data.length >= 2 ? (
<ScrollView contentContainerStyle={styles.scroll} showsVerticalScrollIndicator={false}>
<Card style={styles.tableCard}>
{comparisonRows.map((row) => (
<View
key={row.key}
style={[styles.compareRow, { borderBottomColor: theme.colors.cardBorder }]}
>
<Text style={[styles.rowLabel, { color: theme.colors.textSecondary }]}>
{row.label}
</Text>
<View style={styles.valueColumns}>
{data.map((plan) => (
<View key={`${plan.id}_${row.key}`} style={styles.valueCell}>
<Text style={[styles.valueText, { color: theme.colors.textPrimary }]}>
{row.key === 'premium'
? formatCurrency(plan.premium)
: row.key === 'coverage'
? formatCompactCurrency(plan.coverAmount)
: row.key === 'csr'
? `${plan.claimSettlementRatio}%`
: `${plan.benefits.length} core benefits`}
</Text>
</View>
))}
</View>
</View>
))}
</Card>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.planColumnWrap}>
{data.map((plan) => (
<Card key={plan.id} style={styles.planCard}>
<Text style={[styles.planTitle, { color: theme.colors.textPrimary }]}>
{plan.name}
</Text>
<Text style={[styles.planSubtitle, { color: theme.colors.textSecondary }]}>
{plan.insurerName}
</Text>
{plan.benefits.map((benefit) => (
<Text key={benefit} style={[styles.planBullet, { color: theme.colors.textPrimary }]}>
• {benefit}
</Text>
))}
</Card>
))}
</View>
</ScrollView>
<Button
title="Back to Buy Flow"
onPress={() => navigation.navigate('BuyInsurance', { category: route.params.category })}
/>
</ScrollView>
) : null}
</ScreenShell>
);
}
const styles = StyleSheet.create({
scroll: {
gap: 16,
paddingBottom: 28,
},
tableCard: {
gap: 0,
},
compareRow: {
paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
gap: 10,
},
rowLabel: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
valueColumns: {
flexDirection: 'row',
gap: 12,
},
valueCell: {
flex: 1,
minWidth: 120,
},
valueText: {
fontSize: 14,
fontWeight: '700',
},
planColumnWrap: {
flexDirection: 'row',
gap: 12,
paddingBottom: 4,
},
planCard: {
width: 240,
gap: 8,
},
planTitle: {
fontSize: 15,
fontWeight: '800',
},
planSubtitle: {
fontSize: 12,
fontWeight: '600',
},
planBullet: {
fontSize: 12,
lineHeight: 18,
},
});
@@ -0,0 +1,428 @@
import React from 'react';
import {
Pressable,
RefreshControl,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import Ionicons from '@react-native-vector-icons/ionicons';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { Button, Card, EmptyState, Loader, ScreenShell } from '../../../components';
import type { InsuranceStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
import {
formatCompactCurrency,
formatCurrency,
formatDate,
} from '../../../utils/formatters';
import { InsuranceCategoryChip } from '../components/InsuranceCategoryChip';
import { InsuranceClaimCard } from '../components/InsuranceClaimCard';
import { InsurancePolicyCard } from '../components/InsurancePolicyCard';
import { useInsuranceDashboard } from '../hooks/useInsuranceDashboard';
import { INSURANCE_CATEGORIES } from '../types';
type Props = NativeStackScreenProps<InsuranceStackParamList, 'InsuranceDashboard'>;
export default function InsuranceDashboardScreen({ navigation }: Props) {
const theme = useTheme();
const { dashboard, isLoading, isError, refreshing, onRefresh } = useInsuranceDashboard();
return (
<ScreenShell
title="Insurance"
subtitle="Policies, premiums, claims, and document servicing"
scrollable={false}
rightContent={
<Pressable
onPress={() => navigation.navigate('InsuranceNotifications')}
style={[styles.iconButton, { backgroundColor: theme.colors.surfaceMuted }]}
>
<Ionicons name="notifications-outline" size={20} color={theme.colors.primary} />
</Pressable>
}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={theme.colors.primary}
colors={[theme.colors.primary]}
/>
}
>
{isLoading && !dashboard ? (
<Card>
<Loader />
</Card>
) : null}
{isError && !dashboard ? (
<EmptyState
title="Unable to load insurance"
description="Pull to refresh or try again in a moment."
actionLabel="Retry"
onAction={onRefresh}
/>
) : null}
{dashboard ? (
<>
<Animated.View entering={FadeInDown.springify()}>
<Card style={styles.heroCard}>
<View style={styles.heroHeader}>
<View>
<Text style={[styles.heroEyebrow, { color: theme.colors.primary }]}>
Portfolio overview
</Text>
<Text style={[styles.heroTitle, { color: theme.colors.textPrimary }]}>
{formatCompactCurrency(dashboard.totalCoverageAmount)}
</Text>
<Text style={[styles.heroSubtitle, { color: theme.colors.textSecondary }]}>
Total live coverage across {dashboard.activePoliciesCount} active policies
</Text>
</View>
<View
style={[styles.heroBadge, { backgroundColor: `${theme.colors.accent}14` }]}
>
<Text style={[styles.heroBadgeValue, { color: theme.colors.accent }]}>
{dashboard.policies.length}
</Text>
<Text style={[styles.heroBadgeLabel, { color: theme.colors.textSecondary }]}>
policies
</Text>
</View>
</View>
<View style={styles.metricRow}>
<View style={[styles.metricCard, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.metricLabel, { color: theme.colors.textSecondary }]}>
Upcoming premium
</Text>
<Text style={[styles.metricValue, { color: theme.colors.textPrimary }]}>
{dashboard.upcomingPremium
? formatCurrency(dashboard.upcomingPremium.amount)
: 'None'}
</Text>
<Text style={[styles.metricCaption, { color: theme.colors.textMuted }]}>
{dashboard.upcomingPremium
? formatDate(dashboard.upcomingPremium.dueDate)
: 'No premium due'}
</Text>
</View>
<View style={[styles.metricCard, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.metricLabel, { color: theme.colors.textSecondary }]}>
Due premiums
</Text>
<Text style={[styles.metricValue, { color: theme.colors.textPrimary }]}>
{dashboard.duePremiums.length}
</Text>
<Text style={[styles.metricCaption, { color: theme.colors.textMuted }]}>
Renewal and recurring premium reminders
</Text>
</View>
</View>
</Card>
</Animated.View>
<Animated.View entering={FadeInDown.delay(80).springify()}>
<Card style={styles.sectionCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Quick actions
</Text>
<View style={styles.actionRow}>
<View style={styles.actionCol}>
<Button
title="Buy Policy"
onPress={() => navigation.navigate('BuyInsurance')}
/>
</View>
<View style={styles.actionCol}>
<Button
title="Renew Policy"
variant="secondary"
onPress={() => navigation.navigate('InsuranceRenewals')}
/>
</View>
</View>
<View style={styles.actionRow}>
<View style={styles.actionCol}>
<Button
title="File Claim"
variant="secondary"
onPress={() => navigation.navigate('InsuranceClaims')}
/>
</View>
<View style={styles.actionCol}>
<Button
title="View Policies"
variant="secondary"
onPress={() => navigation.navigate('InsurancePolicies')}
/>
</View>
</View>
<View style={styles.actionRow}>
<View style={styles.actionCol}>
<Button
title="Documents"
variant="secondary"
onPress={() => navigation.navigate('InsuranceDocuments')}
/>
</View>
<View style={styles.actionCol}>
<Button
title="Calculator"
variant="secondary"
onPress={() => navigation.navigate('InsuranceCalculator')}
/>
</View>
</View>
</Card>
</Animated.View>
<Animated.View entering={FadeInDown.delay(120).springify()} style={styles.sectionWrap}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Categories
</Text>
<View style={styles.categoryWrap}>
{INSURANCE_CATEGORIES.map((category) => (
<InsuranceCategoryChip
key={category}
category={category}
selected={false}
onPress={() => navigation.navigate('BuyInsurance', { category })}
/>
))}
</View>
</Animated.View>
<Animated.View entering={FadeInDown.delay(160).springify()} style={styles.sectionWrap}>
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Active policies
</Text>
<Pressable onPress={() => navigation.navigate('InsurancePolicies')}>
<Text style={[styles.linkText, { color: theme.colors.primary }]}>See all</Text>
</Pressable>
</View>
{dashboard.policies.map((policy) => (
<InsurancePolicyCard
key={policy.id}
policy={policy}
onPress={() =>
navigation.navigate('InsurancePolicyDetails', { policyId: policy.id })
}
/>
))}
</Animated.View>
<Animated.View entering={FadeInDown.delay(200).springify()} style={styles.sectionWrap}>
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Recent claims
</Text>
<Pressable onPress={() => navigation.navigate('InsuranceClaims')}>
<Text style={[styles.linkText, { color: theme.colors.primary }]}>Manage</Text>
</Pressable>
</View>
{dashboard.recentClaims.length ? (
dashboard.recentClaims.map((claim) => (
<InsuranceClaimCard key={claim.id} claim={claim} />
))
) : (
<EmptyState
title="No claims yet"
description="Your submitted claims and settlement updates will appear here."
/>
)}
</Animated.View>
<Animated.View entering={FadeInDown.delay(240).springify()} style={styles.sectionWrap}>
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Notifications
</Text>
<Pressable onPress={() => navigation.navigate('InsuranceNotifications')}>
<Text style={[styles.linkText, { color: theme.colors.primary }]}>Open</Text>
</Pressable>
</View>
<Card style={styles.notificationCard}>
{dashboard.notifications.map((notification) => (
<View
key={notification.id}
style={[styles.notificationRow, { borderBottomColor: theme.colors.cardBorder }]}
>
<View style={styles.notificationCopy}>
<Text style={[styles.notificationTitle, { color: theme.colors.textPrimary }]}>
{notification.title}
</Text>
<Text style={[styles.notificationBody, { color: theme.colors.textSecondary }]}>
{notification.body}
</Text>
</View>
{!notification.read ? (
<View
style={[styles.unreadDot, { backgroundColor: theme.colors.primary }]}
/>
) : null}
</View>
))}
</Card>
</Animated.View>
</>
) : null}
<View style={styles.bottomSpacer} />
</ScrollView>
</ScreenShell>
);
}
const styles = StyleSheet.create({
scrollContent: {
gap: 18,
paddingBottom: 24,
},
iconButton: {
width: 40,
height: 40,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
heroCard: {
gap: 16,
},
heroHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 12,
},
heroEyebrow: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.6,
},
heroTitle: {
fontSize: 28,
fontWeight: '900',
marginTop: 4,
},
heroSubtitle: {
fontSize: 13,
lineHeight: 18,
marginTop: 4,
},
heroBadge: {
minWidth: 82,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 12,
paddingVertical: 12,
},
heroBadgeValue: {
fontSize: 22,
fontWeight: '900',
},
heroBadgeLabel: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
metricRow: {
flexDirection: 'row',
gap: 12,
},
metricCard: {
flex: 1,
borderRadius: 16,
padding: 14,
gap: 4,
},
metricLabel: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
metricValue: {
fontSize: 18,
fontWeight: '900',
},
metricCaption: {
fontSize: 11,
fontWeight: '600',
lineHeight: 16,
},
sectionCard: {
gap: 14,
},
sectionTitle: {
fontSize: 17,
fontWeight: '800',
},
actionRow: {
flexDirection: 'row',
gap: 12,
},
actionCol: {
flex: 1,
},
sectionWrap: {
gap: 12,
},
categoryWrap: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 10,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
linkText: {
fontSize: 12,
fontWeight: '800',
},
notificationCard: {
gap: 0,
},
notificationRow: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: 10,
paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
},
notificationCopy: {
flex: 1,
gap: 4,
},
notificationTitle: {
fontSize: 14,
fontWeight: '800',
},
notificationBody: {
fontSize: 12,
lineHeight: 17,
},
unreadDot: {
width: 8,
height: 8,
borderRadius: 4,
marginTop: 6,
},
bottomSpacer: {
height: 70,
},
});
@@ -0,0 +1,180 @@
import React from 'react';
import {
FlatList,
RefreshControl,
StyleSheet,
Text,
View,
} from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { Button, Card, EmptyState, Loader, ScreenShell } from '../../../components';
import { useToast } from '../../../hooks/useToast';
import type { InsuranceStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
import { InsuranceDocumentItem } from '../components/InsuranceDocumentItem';
import { useGetInsuranceDocumentsQuery, useUploadInsuranceDocumentMutation } from '../services/insuranceApi';
type Props = NativeStackScreenProps<InsuranceStackParamList, 'InsuranceDocuments'>;
const SAMPLE_UPLOADS = [
{
name: 'Aadhaar Front',
type: 'Aadhaar' as const,
category: 'kyc' as const,
mimeType: 'application/pdf',
uri: 'https://mock.bankcbs.dev/uploads/aadhaar_front.pdf',
sizeKb: 224,
},
{
name: 'PAN Card',
type: 'PAN' as const,
category: 'kyc' as const,
mimeType: 'image/jpeg',
uri: 'https://mock.bankcbs.dev/uploads/pan_card.jpg',
sizeKb: 188,
},
{
name: 'Claim Support PDF',
type: 'Invoice' as const,
category: 'claim' as const,
mimeType: 'application/pdf',
uri: 'https://mock.bankcbs.dev/uploads/claim_support.pdf',
sizeKb: 464,
},
];
export default function InsuranceDocumentsScreen(_props: Props) {
const theme = useTheme();
const toast = useToast();
const { data, isLoading, isError, refetch, isFetching } = useGetInsuranceDocumentsQuery();
const [uploadDocument, uploadState] = useUploadInsuranceDocumentMutation();
const handleUpload = async (index: number) => {
const sample = SAMPLE_UPLOADS[index];
if (!sample) {
return;
}
const result = await uploadDocument(sample);
if ('data' in result) {
toast('Document uploaded', 'success', `${sample.name} is now available in the secure vault.`);
} else {
toast('Upload failed', 'error', 'Please try again.');
}
};
return (
<ScreenShell title="Documents" subtitle="KYC vault and claim supporting files" scrollable={false}>
<FlatList
data={data ?? []}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={isFetching}
onRefresh={refetch}
tintColor={theme.colors.primary}
colors={[theme.colors.primary]}
/>
}
ListHeaderComponent={
<View style={styles.headerWrap}>
<Card style={styles.summaryCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Upload shortcuts
</Text>
<View style={styles.actionRow}>
<View style={styles.actionCol}>
<Button
title="Upload Aadhaar"
variant="secondary"
onPress={() => void handleUpload(0)}
loading={uploadState.isLoading}
/>
</View>
<View style={styles.actionCol}>
<Button
title="Upload PAN"
variant="secondary"
onPress={() => void handleUpload(1)}
loading={uploadState.isLoading}
/>
</View>
</View>
<Button
title="Upload Claim PDF"
variant="secondary"
onPress={() => void handleUpload(2)}
loading={uploadState.isLoading}
/>
</Card>
</View>
}
renderItem={({ item }) => (
<InsuranceDocumentItem
document={item}
onView={() =>
toast('Document available', 'info', `${item.name} is stored in encrypted vault storage.`)
}
onReupload={() =>
void uploadDocument({
...SAMPLE_UPLOADS[item.category === 'kyc' ? 0 : 2]!,
existingDocumentId: item.id,
})
}
/>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
ListEmptyComponent={
isLoading ? (
<Card>
<Loader />
</Card>
) : isError ? (
<EmptyState
title="Unable to load documents"
description="Please refresh and try again."
actionLabel="Retry"
onAction={refetch}
/>
) : (
<EmptyState
title="No documents uploaded"
description="Upload your KYC and claim files to keep them ready for servicing."
/>
)
}
/>
</ScreenShell>
);
}
const styles = StyleSheet.create({
listContent: {
paddingBottom: 70,
},
headerWrap: {
marginBottom: 16,
},
summaryCard: {
gap: 12,
},
sectionTitle: {
fontSize: 16,
fontWeight: '800',
},
actionRow: {
flexDirection: 'row',
gap: 12,
},
actionCol: {
flex: 1,
},
separator: {
height: 12,
},
});
@@ -0,0 +1,131 @@
import React from 'react';
import {
FlatList,
Pressable,
RefreshControl,
StyleSheet,
Text,
View,
} from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import Ionicons from '@react-native-vector-icons/ionicons';
import { Card, EmptyState, Loader, ScreenShell } from '../../../components';
import type { InsuranceStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
import { formatDate } from '../../../utils/formatters';
import {
useGetInsuranceNotificationsQuery,
useMarkInsuranceNotificationReadMutation,
} from '../services/insuranceApi';
type Props = NativeStackScreenProps<InsuranceStackParamList, 'InsuranceNotifications'>;
export default function InsuranceNotificationsScreen(_props: Props) {
const theme = useTheme();
const { data, isLoading, isError, refetch, isFetching } = useGetInsuranceNotificationsQuery();
const [markRead] = useMarkInsuranceNotificationReadMutation();
return (
<ScreenShell title="Notifications" subtitle="Premiums, renewals, claim updates, and offers">
<FlatList
data={data ?? []}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={isFetching}
onRefresh={refetch}
tintColor={theme.colors.primary}
colors={[theme.colors.primary]}
/>
}
renderItem={({ item }) => (
<Pressable onPress={() => void markRead(item.id)}>
<Card style={styles.card}>
<View style={styles.row}>
<View style={styles.copy}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>
{item.title}
</Text>
<Text style={[styles.body, { color: theme.colors.textSecondary }]}>
{item.body}
</Text>
<Text style={[styles.date, { color: theme.colors.textMuted }]}>
{formatDate(item.createdAt)}
</Text>
</View>
{!item.read ? (
<View style={[styles.unreadWrap, { backgroundColor: `${theme.colors.primary}12` }]}>
<Ionicons name="ellipse" size={10} color={theme.colors.primary} />
</View>
) : null}
</View>
</Card>
</Pressable>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
ListEmptyComponent={
isLoading ? (
<Card>
<Loader />
</Card>
) : isError ? (
<EmptyState
title="Unable to load notifications"
description="Please refresh and try again."
actionLabel="Retry"
onAction={refetch}
/>
) : (
<EmptyState
title="You're all caught up"
description="New premium reminders and claim updates will land here."
/>
)
}
/>
</ScreenShell>
);
}
const styles = StyleSheet.create({
listContent: {
paddingBottom: 70,
},
card: {
gap: 0,
},
row: {
flexDirection: 'row',
gap: 10,
},
copy: {
flex: 1,
gap: 4,
},
title: {
fontSize: 15,
fontWeight: '800',
},
body: {
fontSize: 12,
lineHeight: 18,
},
date: {
fontSize: 11,
fontWeight: '600',
},
unreadWrap: {
width: 26,
height: 26,
borderRadius: 13,
alignItems: 'center',
justifyContent: 'center',
marginTop: 2,
},
separator: {
height: 12,
},
});
@@ -0,0 +1,125 @@
import React, { useMemo } from 'react';
import {
FlatList,
RefreshControl,
StyleSheet,
Text,
View,
} from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { Button, Card, EmptyState, Loader, ScreenShell } from '../../../components';
import type { InsuranceStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
import { formatCompactCurrency } from '../../../utils/formatters';
import { InsurancePolicyCard } from '../components/InsurancePolicyCard';
import { useGetInsurancePoliciesQuery } from '../services/insuranceApi';
type Props = NativeStackScreenProps<InsuranceStackParamList, 'InsurancePolicies'>;
export default function InsurancePoliciesScreen({ navigation }: Props) {
const theme = useTheme();
const { data, isLoading, isError, refetch, isFetching } = useGetInsurancePoliciesQuery();
const summary = useMemo(() => {
const policies = data ?? [];
return {
totalPolicies: policies.length,
totalCoverage: policies.reduce((sum, policy) => sum + policy.coverageAmount, 0),
};
}, [data]);
return (
<ScreenShell title="Policies" subtitle="All policy contracts and renewals" scrollable={false}>
<FlatList
data={data ?? []}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={isFetching}
onRefresh={refetch}
tintColor={theme.colors.primary}
colors={[theme.colors.primary]}
/>
}
ListHeaderComponent={
<View style={styles.headerWrap}>
<Card style={styles.summaryCard}>
<Text style={[styles.summaryLabel, { color: theme.colors.textSecondary }]}>
Portfolio coverage
</Text>
<Text style={[styles.summaryValue, { color: theme.colors.textPrimary }]}>
{formatCompactCurrency(summary.totalCoverage)}
</Text>
<Text style={[styles.summaryCaption, { color: theme.colors.textMuted }]}>
{summary.totalPolicies} policies across active, due, and historical contracts
</Text>
</Card>
<Button title="Buy New Policy" onPress={() => navigation.navigate('BuyInsurance')} />
</View>
}
renderItem={({ item }) => (
<InsurancePolicyCard
policy={item}
onPress={() => navigation.navigate('InsurancePolicyDetails', { policyId: item.id })}
/>
)}
ListEmptyComponent={
isLoading ? (
<Card>
<Loader />
</Card>
) : isError ? (
<EmptyState
title="Unable to load policies"
description="Please refresh and try again."
actionLabel="Retry"
onAction={refetch}
/>
) : (
<EmptyState
title="No policies found"
description="Buy your first policy to start building your protection portfolio."
actionLabel="Buy Policy"
onAction={() => navigation.navigate('BuyInsurance')}
/>
)
}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
</ScreenShell>
);
}
const styles = StyleSheet.create({
listContent: {
gap: 12,
paddingBottom: 70,
},
headerWrap: {
gap: 14,
},
summaryCard: {
gap: 4,
},
summaryLabel: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
summaryValue: {
fontSize: 26,
fontWeight: '900',
},
summaryCaption: {
fontSize: 12,
lineHeight: 17,
},
separator: {
height: 2,
},
});
@@ -0,0 +1,339 @@
import React from 'react';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import Ionicons from '@react-native-vector-icons/ionicons';
import { Button, Card, EmptyState, Loader, ScreenShell } from '../../../components';
import { useToast } from '../../../hooks/useToast';
import type { InsuranceStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
import { formatCompactCurrency, formatCurrency, formatDate } from '../../../utils/formatters';
import { InsuranceStatusPill } from '../components/InsuranceStatusPill';
import { useGetInsurancePolicyDetailQuery } from '../services/insuranceApi';
type Props = NativeStackScreenProps<InsuranceStackParamList, 'InsurancePolicyDetails'>;
export default function InsurancePolicyDetailsScreen({ navigation, route }: Props) {
const theme = useTheme();
const toast = useToast();
const { data, isLoading, isError, refetch } = useGetInsurancePolicyDetailQuery(
route.params.policyId,
);
return (
<ScreenShell title="Policy Details" subtitle="Coverage, insurer, and servicing actions">
{isLoading ? (
<Card>
<Loader />
</Card>
) : null}
{isError || !data ? (
<EmptyState
title="Unable to load policy"
description="Please retry in a moment."
actionLabel="Retry"
onAction={refetch}
/>
) : (
<ScrollView contentContainerStyle={styles.scroll} showsVerticalScrollIndicator={false}>
<Card style={styles.heroCard}>
<View style={styles.heroHeader}>
<View style={styles.heroCopy}>
<Text style={[styles.heroTitle, { color: theme.colors.textPrimary }]}>
{data.planName}
</Text>
<Text style={[styles.heroMeta, { color: theme.colors.textSecondary }]}>
{data.policyNumber} · {data.insurer.name}
</Text>
</View>
<InsuranceStatusPill status={data.status} />
</View>
<View style={styles.metricGrid}>
<View style={[styles.metricCard, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.metricLabel, { color: theme.colors.textSecondary }]}>
Coverage
</Text>
<Text style={[styles.metricValue, { color: theme.colors.textPrimary }]}>
{formatCompactCurrency(data.coverageAmount)}
</Text>
</View>
<View style={[styles.metricCard, { backgroundColor: theme.colors.surfaceMuted }]}>
<Text style={[styles.metricLabel, { color: theme.colors.textSecondary }]}>
Premium
</Text>
<Text style={[styles.metricValue, { color: theme.colors.textPrimary }]}>
{formatCurrency(data.premiumAmount)}
</Text>
<Text style={[styles.metricCaption, { color: theme.colors.textMuted }]}>
{data.premiumFrequency}
</Text>
</View>
</View>
<View style={styles.infoList}>
<Text style={[styles.infoRow, { color: theme.colors.textSecondary }]}>
Start date:{' '}
<Text style={[styles.infoStrong, { color: theme.colors.textPrimary }]}>
{formatDate(data.startDate)}
</Text>
</Text>
<Text style={[styles.infoRow, { color: theme.colors.textSecondary }]}>
Expiry date:{' '}
<Text style={[styles.infoStrong, { color: theme.colors.textPrimary }]}>
{formatDate(data.expiryDate)}
</Text>
</Text>
<Text style={[styles.infoRow, { color: theme.colors.textSecondary }]}>
Next premium due:{' '}
<Text style={[styles.infoStrong, { color: theme.colors.textPrimary }]}>
{formatDate(data.nextPremiumDueDate)}
</Text>
</Text>
<Text style={[styles.infoRow, { color: theme.colors.textSecondary }]}>
Nominee:{' '}
<Text style={[styles.infoStrong, { color: theme.colors.textPrimary }]}>
{data.nomineeName} ({data.nomineeRelationship})
</Text>
</Text>
</View>
</Card>
<Card style={styles.actionCard}>
<View style={styles.actionRow}>
<View style={styles.actionCol}>
<Button
title="Pay Premium"
onPress={() =>
navigation.navigate('InsuranceRenewals', { policyId: data.id })
}
/>
</View>
<View style={styles.actionCol}>
<Button
title="File Claim"
variant="secondary"
onPress={() =>
navigation.navigate('InsuranceClaims', { policyId: data.id })
}
/>
</View>
</View>
<Button
title="Download Policy PDF"
variant="secondary"
onPress={() =>
toast(
'Policy download ready',
'success',
`${data.documents[0]?.name ?? 'Policy schedule'} is ready from the secure vault.`,
)
}
/>
</Card>
<Card style={styles.sectionCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Benefits and inclusions
</Text>
{data.benefits.map((benefit) => (
<View key={benefit} style={styles.listRow}>
<Ionicons name="checkmark-circle" size={16} color={theme.colors.accent} />
<Text style={[styles.listText, { color: theme.colors.textPrimary }]}>
{benefit}
</Text>
</View>
))}
{data.inclusions.map((inclusion) => (
<View key={inclusion} style={styles.listRow}>
<Ionicons name="shield-checkmark" size={16} color={theme.colors.primary} />
<Text style={[styles.listText, { color: theme.colors.textPrimary }]}>
{inclusion}
</Text>
</View>
))}
</Card>
<Card style={styles.sectionCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Exclusions
</Text>
{data.exclusions.map((exclusion) => (
<View key={exclusion} style={styles.listRow}>
<Ionicons name="close-circle" size={16} color={theme.colors.error} />
<Text style={[styles.listText, { color: theme.colors.textPrimary }]}>
{exclusion}
</Text>
</View>
))}
</Card>
<Card style={styles.sectionCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Insurer details
</Text>
<Text style={[styles.infoRow, { color: theme.colors.textSecondary }]}>
Claims ratio:{' '}
<Text style={[styles.infoStrong, { color: theme.colors.textPrimary }]}>
{data.insurer.claimSettlementRatio}%
</Text>
</Text>
<Text style={[styles.infoRow, { color: theme.colors.textSecondary }]}>
Support:{' '}
<Text style={[styles.infoStrong, { color: theme.colors.textPrimary }]}>
{data.insurer.supportPhone}
</Text>
</Text>
<Text style={[styles.infoRow, { color: theme.colors.textSecondary }]}>
Email:{' '}
<Text style={[styles.infoStrong, { color: theme.colors.textPrimary }]}>
{data.insurer.supportEmail}
</Text>
</Text>
<Text style={[styles.infoRow, { color: theme.colors.textSecondary }]}>
Address:{' '}
<Text style={[styles.infoStrong, { color: theme.colors.textPrimary }]}>
{data.insurer.address}
</Text>
</Text>
</Card>
<Card style={styles.sectionCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Policy documents
</Text>
{data.documents.map((document) => (
<Pressable
key={document.id}
onPress={() =>
toast('Document available', 'info', `${document.name} is available in PDF.`)
}
style={[styles.documentRow, { backgroundColor: theme.colors.surfaceMuted }]}
>
<View style={styles.documentCopy}>
<Text style={[styles.documentName, { color: theme.colors.textPrimary }]}>
{document.name}
</Text>
<Text style={[styles.documentMeta, { color: theme.colors.textMuted }]}>
{document.mimeType} · {document.sizeKb} KB
</Text>
</View>
<Ionicons name="download-outline" size={18} color={theme.colors.primary} />
</Pressable>
))}
</Card>
</ScrollView>
)}
<View style={{ height: theme.spacing.xxl }} />
</ScreenShell>
);
}
const styles = StyleSheet.create({
scroll: {
gap: 16,
paddingBottom: 32,
},
heroCard: {
gap: 14,
},
heroHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 12,
},
heroCopy: {
flex: 1,
gap: 2,
},
heroTitle: {
fontSize: 18,
fontWeight: '800',
},
heroMeta: {
fontSize: 12,
fontWeight: '600',
},
metricGrid: {
flexDirection: 'row',
gap: 12,
},
metricCard: {
flex: 1,
borderRadius: 16,
padding: 14,
gap: 4,
},
metricLabel: {
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
metricValue: {
fontSize: 18,
fontWeight: '900',
},
metricCaption: {
fontSize: 11,
fontWeight: '600',
},
infoList: {
gap: 6,
},
infoRow: {
fontSize: 12,
lineHeight: 18,
},
infoStrong: {
fontWeight: '700',
},
actionCard: {
gap: 12,
},
actionRow: {
flexDirection: 'row',
gap: 12,
},
actionCol: {
flex: 1,
},
sectionCard: {
gap: 10,
},
sectionTitle: {
fontSize: 16,
fontWeight: '800',
},
listRow: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: 8,
},
listText: {
flex: 1,
fontSize: 12,
lineHeight: 18,
fontWeight: '600',
},
documentRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderRadius: 14,
padding: 12,
},
documentCopy: {
flex: 1,
gap: 3,
},
documentName: {
fontSize: 14,
fontWeight: '700',
},
documentMeta: {
fontSize: 11,
fontWeight: '600',
},
});
@@ -0,0 +1,297 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
FlatList,
RefreshControl,
StyleSheet,
Text,
View,
} from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { Button, Card, EmptyState, Loader, ScreenShell } from '../../../components';
import { ToggleSwitch } from '../../cards/components/ToggleSwitch';
import { useToast } from '../../../hooks/useToast';
import type { InsuranceStackParamList } from '../../../navigation/types';
import { useTheme } from '../../../theme';
import { formatCurrency, formatDate } from '../../../utils/formatters';
import { InsuranceOtpModal } from '../components/InsuranceOtpModal';
import { InsurancePaymentMethodTile } from '../components/InsurancePaymentMethodTile';
import { InsuranceStatusPill } from '../components/InsuranceStatusPill';
import {
useGetInsurancePremiumDuesQuery,
usePayInsurancePremiumMutation,
useRequestInsuranceOtpMutation,
useToggleInsurancePolicyAutoRenewMutation,
} from '../services/insuranceApi';
import type { InsurancePaymentMethod, InsurancePremiumDue } from '../types';
import { validateInsuranceOtp } from '../utils/insuranceValidation';
type Props = NativeStackScreenProps<InsuranceStackParamList, 'InsuranceRenewals'>;
export default function InsuranceRenewalsScreen({ route }: Props) {
const theme = useTheme();
const toast = useToast();
const { data, isLoading, isError, refetch, isFetching } = useGetInsurancePremiumDuesQuery();
const [toggleAutoRenew] = useToggleInsurancePolicyAutoRenewMutation();
const [requestOtp, requestOtpState] = useRequestInsuranceOtpMutation();
const [payPremium, payPremiumState] = usePayInsurancePremiumMutation();
const [selectedDueId, setSelectedDueId] = useState<string | null>(route.params?.policyId ?? null);
const [paymentMethod, setPaymentMethod] = useState<InsurancePaymentMethod>('UPI');
const [otp, setOtp] = useState('');
const [otpError, setOtpError] = useState<string | undefined>();
const [otpVisible, setOtpVisible] = useState(false);
const [mockCodeHint, setMockCodeHint] = useState<string | undefined>();
useEffect(() => {
if (route.params?.policyId) {
setSelectedDueId(route.params.policyId);
}
}, [route.params?.policyId]);
const filteredDues = useMemo(() => {
if (!route.params?.policyId) {
return data ?? [];
}
return (data ?? []).filter((due) => due.policyId === route.params?.policyId);
}, [data, route.params?.policyId]);
const selectedDue = filteredDues.find((due) => due.policyId === selectedDueId) ?? null;
const handleToggleAutoRenew = async (due: InsurancePremiumDue, enabled: boolean) => {
const result = await toggleAutoRenew({ policyId: due.policyId, enabled });
if ('data' in result && result.data) {
toast(
enabled ? 'Auto-renew enabled' : 'Auto-renew disabled',
'success',
`${due.planName} has been updated.`,
);
} else {
toast('Unable to update auto-renew', 'error', 'Please try again.');
}
};
const handleRequestOtp = async () => {
if (!selectedDue) {
toast('Select a premium', 'info', 'Choose a due premium to continue.');
return;
}
const result = await requestOtp({ action: 'payment', policyId: selectedDue.policyId });
if ('data' in result && result.data) {
setMockCodeHint(result.data.mockCodeHint);
setOtpVisible(true);
setOtpError(undefined);
return;
}
toast('Unable to send OTP', 'error', 'Please try again.');
};
const handleConfirmPayment = async () => {
if (!selectedDue) {
return;
}
const error = validateInsuranceOtp(otp);
if (error) {
setOtpError(error);
return;
}
const result = await payPremium({
policyId: selectedDue.policyId,
amount: selectedDue.amount,
method: paymentMethod,
otp,
});
if ('data' in result && result.data) {
setOtpVisible(false);
setOtp('');
setOtpError(undefined);
toast(
'Premium paid',
'success',
`Next premium due on ${formatDate(result.data.nextDueDate)}.`,
);
return;
}
setOtpError('OTP verification failed. Please try again.');
};
return (
<ScreenShell title="Renewals" subtitle="Pay premiums and manage auto-renew">
<FlatList
data={filteredDues}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={isFetching}
onRefresh={refetch}
tintColor={theme.colors.primary}
colors={[theme.colors.primary]}
/>
}
ListHeaderComponent={
selectedDue ? (
<Card style={styles.paymentCard}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Payment method
</Text>
<InsurancePaymentMethodTile
method="UPI"
selected={paymentMethod === 'UPI'}
onPress={() => setPaymentMethod('UPI')}
/>
<InsurancePaymentMethodTile
method="Net Banking"
selected={paymentMethod === 'Net Banking'}
onPress={() => setPaymentMethod('Net Banking')}
/>
<InsurancePaymentMethodTile
method="Card"
selected={paymentMethod === 'Card'}
onPress={() => setPaymentMethod('Card')}
/>
<Button
title="Request OTP"
onPress={() => void handleRequestOtp()}
loading={requestOtpState.isLoading}
/>
</Card>
) : null
}
renderItem={({ item }) => (
<Card style={styles.dueCard}>
<View style={styles.cardHeader}>
<View style={styles.cardCopy}>
<Text style={[styles.planTitle, { color: theme.colors.textPrimary }]}>
{item.planName}
</Text>
<Text style={[styles.planMeta, { color: theme.colors.textSecondary }]}>
{item.policyNumber} · Due {formatDate(item.dueDate)}
</Text>
</View>
<InsuranceStatusPill status={item.lateFee ? 'Grace Period' : 'Renewal Due'} />
</View>
<View style={styles.metricRow}>
<Text style={[styles.metricLabel, { color: theme.colors.textMuted }]}>Amount</Text>
<Text style={[styles.metricValue, { color: theme.colors.textPrimary }]}>
{formatCurrency(item.amount)}
</Text>
</View>
<View style={styles.metricRow}>
<Text style={[styles.metricLabel, { color: theme.colors.textMuted }]}>Late fee</Text>
<Text style={[styles.metricValue, { color: theme.colors.textPrimary }]}>
{formatCurrency(item.lateFee)}
</Text>
</View>
<ToggleSwitch
label="Auto-renew"
description="Automatically renew this policy on the due date."
value={item.autoRenewEnabled}
onValueChange={(value) => {
void handleToggleAutoRenew(item, value);
}}
/>
<Button
title={selectedDueId === item.policyId ? 'Selected for payment' : 'Pay this premium'}
variant={selectedDueId === item.policyId ? 'secondary' : 'primary'}
onPress={() => setSelectedDueId(item.policyId)}
/>
</Card>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
ListEmptyComponent={
isLoading ? (
<Card>
<Loader />
</Card>
) : isError ? (
<EmptyState
title="Unable to load due premiums"
description="Refresh and try again."
actionLabel="Retry"
onAction={refetch}
/>
) : (
<EmptyState
title="No premiums due"
description="Your upcoming premium reminders will appear here."
/>
)
}
/>
<InsuranceOtpModal
visible={otpVisible}
title="Verify Premium Payment"
description="Enter the OTP sent to your registered mobile number to authorize this premium payment."
otp={otp}
onOtpChange={setOtp}
onClose={() => setOtpVisible(false)}
onConfirm={() => void handleConfirmPayment()}
loading={payPremiumState.isLoading}
error={otpError}
mockCodeHint={mockCodeHint}
/>
</ScreenShell>
);
}
const styles = StyleSheet.create({
listContent: {
paddingBottom: 70,
},
paymentCard: {
gap: 12,
marginBottom: 16,
},
sectionTitle: {
fontSize: 16,
fontWeight: '800',
},
dueCard: {
gap: 12,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 12,
},
cardCopy: {
flex: 1,
gap: 2,
},
planTitle: {
fontSize: 15,
fontWeight: '800',
},
planMeta: {
fontSize: 12,
fontWeight: '600',
},
metricRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
metricLabel: {
fontSize: 12,
fontWeight: '600',
},
metricValue: {
fontSize: 14,
fontWeight: '800',
},
separator: {
height: 12,
},
});
@@ -0,0 +1,314 @@
import { appConfig } from '../../../config/appConfig';
import { apiClient } from '../../../services/api/apiClient';
import { baseApi } from '../../../services/api/baseApi';
import { endpoints } from '../../../services/api/endpoints';
import { toApiError } from '../../../services/api/serviceHelpers';
import { sleep } from '../../../utils/async';
import type {
InsuranceCalculatorEstimate,
InsuranceCalculatorInput,
InsuranceClaim,
InsuranceDashboardPayload,
InsuranceDocument,
InsuranceNotification,
InsuranceOtpSession,
InsurancePlan,
InsurancePolicy,
InsurancePremiumDue,
PayInsurancePremiumPayload,
PayInsurancePremiumResult,
PurchaseInsurancePayload,
PurchaseInsuranceResult,
RequestInsuranceOtpPayload,
SubmitInsuranceClaimPayload,
SubmitInsuranceClaimResult,
UploadInsuranceDocumentPayload,
} from '../types';
import {
compareInsurancePlans,
getInsuranceDashboard,
getInsurancePolicyDetail,
listInsuranceClaims,
listInsuranceDocuments,
listInsuranceNotifications,
listInsurancePlans,
listInsurancePolicies,
markInsuranceNotificationRead,
payInsurancePremium,
purchaseInsurancePolicy,
requestInsuranceOtp,
seedInsurancePlans as seedInsurancePlanCatalog,
submitInsuranceClaim,
toggleInsurancePolicyAutoRenew,
uploadInsuranceDocument,
} from './mockInsuranceData';
import { estimateInsurancePremium } from '../utils/insuranceMath';
const handleMock = async <T>(factory: () => T, delayMs = 320) => {
await sleep(delayMs);
try {
return { data: factory() };
} catch (error) {
return { error: toApiError(error) };
}
};
const handleRequest = async <T>(request: () => Promise<T>) => {
try {
return { data: await request() };
} catch (error) {
return { error: toApiError(error) };
}
};
export { seedInsurancePlanCatalog };
export const insuranceApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getInsuranceDashboard: builder.query<InsuranceDashboardPayload, void>({
queryFn: async () =>
appConfig.features.useMockServer
? handleMock(() => getInsuranceDashboard())
: handleRequest(async () => {
const response = await apiClient.get<InsuranceDashboardPayload>(
endpoints.insurance.dashboard,
);
return response.data;
}),
providesTags: ['Insurance'],
}),
getInsurancePlans: builder.query<InsurancePlan[], { category?: string } | void>({
queryFn: async (args) =>
appConfig.features.useMockServer
? handleMock(() => listInsurancePlans(args?.category as never))
: handleRequest(async () => {
const response = await apiClient.get<InsurancePlan[]>(endpoints.insurance.plans, {
params: args?.category ? { category: args.category } : undefined,
});
return response.data;
}),
providesTags: ['Insurance'],
}),
compareInsurancePlans: builder.query<InsurancePlan[], string[]>({
queryFn: async (planIds) =>
appConfig.features.useMockServer
? handleMock(() => compareInsurancePlans(planIds))
: handleRequest(async () => {
const response = await apiClient.post<InsurancePlan[]>(
endpoints.insurance.comparePlans,
{ planIds },
);
return response.data;
}),
providesTags: ['Insurance'],
}),
estimateInsurancePremium: builder.query<InsuranceCalculatorEstimate, InsuranceCalculatorInput>({
queryFn: async (input) =>
appConfig.features.useMockServer
? handleMock(() => estimateInsurancePremium(input), 150)
: handleRequest(async () => {
const response = await apiClient.post<InsuranceCalculatorEstimate>(
endpoints.insurance.calculator,
input,
);
return response.data;
}),
providesTags: ['Insurance'],
}),
getInsurancePolicies: builder.query<InsurancePolicy[], void>({
queryFn: async () =>
appConfig.features.useMockServer
? handleMock(() => listInsurancePolicies())
: handleRequest(async () => {
const response = await apiClient.get<InsurancePolicy[]>(endpoints.insurance.policies);
return response.data;
}),
providesTags: ['Insurance'],
}),
getInsurancePolicyDetail: builder.query<InsurancePolicy, string>({
queryFn: async (policyId) =>
appConfig.features.useMockServer
? handleMock(() => getInsurancePolicyDetail(policyId))
: handleRequest(async () => {
const response = await apiClient.get<InsurancePolicy>(
endpoints.insurance.policyDetail(policyId),
);
return response.data;
}),
providesTags: ['Insurance'],
}),
getInsurancePremiumDues: builder.query<InsurancePremiumDue[], void>({
queryFn: async () =>
appConfig.features.useMockServer
? handleMock(() => getInsuranceDashboard().duePremiums)
: handleRequest(async () => {
const response = await apiClient.get<InsurancePremiumDue[]>(
endpoints.insurance.premiumDues,
);
return response.data;
}),
providesTags: ['Insurance'],
}),
requestInsuranceOtp: builder.mutation<InsuranceOtpSession, RequestInsuranceOtpPayload>({
queryFn: async (payload) =>
appConfig.features.useMockServer
? handleMock(() => requestInsuranceOtp(payload), 220)
: handleRequest(async () => {
const response = await apiClient.post<InsuranceOtpSession>(
endpoints.insurance.otp,
payload,
);
return response.data;
}),
}),
purchaseInsurancePolicy: builder.mutation<PurchaseInsuranceResult, PurchaseInsurancePayload>({
queryFn: async (payload) =>
appConfig.features.useMockServer
? handleMock(() => purchaseInsurancePolicy(payload), 700)
: handleRequest(async () => {
const response = await apiClient.post<PurchaseInsuranceResult>(
endpoints.insurance.purchase,
payload,
);
return response.data;
}),
invalidatesTags: ['Insurance'],
}),
payInsurancePremium: builder.mutation<PayInsurancePremiumResult, PayInsurancePremiumPayload>({
queryFn: async (payload) =>
appConfig.features.useMockServer
? handleMock(() => payInsurancePremium(payload), 650)
: handleRequest(async () => {
const response = await apiClient.post<PayInsurancePremiumResult>(
endpoints.insurance.payPremium,
payload,
);
return response.data;
}),
invalidatesTags: ['Insurance'],
}),
toggleInsurancePolicyAutoRenew: builder.mutation<
InsurancePolicy,
{ policyId: string; enabled: boolean }
>({
queryFn: async (payload) =>
appConfig.features.useMockServer
? handleMock(
() => toggleInsurancePolicyAutoRenew(payload.policyId, payload.enabled),
180,
)
: handleRequest(async () => {
const response = await apiClient.patch<InsurancePolicy>(
endpoints.insurance.autoRenew(payload.policyId),
{ enabled: payload.enabled },
);
return response.data;
}),
invalidatesTags: ['Insurance'],
}),
getInsuranceClaims: builder.query<InsuranceClaim[], { policyId?: string } | void>({
queryFn: async (args) =>
appConfig.features.useMockServer
? handleMock(() => listInsuranceClaims(args?.policyId))
: handleRequest(async () => {
const response = await apiClient.get<InsuranceClaim[]>(endpoints.insurance.claims, {
params: args?.policyId ? { policyId: args.policyId } : undefined,
});
return response.data;
}),
providesTags: ['Insurance'],
}),
submitInsuranceClaim: builder.mutation<
SubmitInsuranceClaimResult,
SubmitInsuranceClaimPayload
>({
queryFn: async (payload) =>
appConfig.features.useMockServer
? handleMock(() => submitInsuranceClaim(payload), 680)
: handleRequest(async () => {
const response = await apiClient.post<SubmitInsuranceClaimResult>(
endpoints.insurance.submitClaim,
payload,
);
return response.data;
}),
invalidatesTags: ['Insurance'],
}),
getInsuranceDocuments: builder.query<
InsuranceDocument[],
{ category?: 'kyc' | 'claim' } | void
>({
queryFn: async (args) =>
appConfig.features.useMockServer
? handleMock(() => listInsuranceDocuments(args?.category))
: handleRequest(async () => {
const response = await apiClient.get<InsuranceDocument[]>(
endpoints.insurance.documents,
{
params: args?.category ? { category: args.category } : undefined,
},
);
return response.data;
}),
providesTags: ['Insurance'],
}),
uploadInsuranceDocument: builder.mutation<InsuranceDocument, UploadInsuranceDocumentPayload>({
queryFn: async (payload) =>
appConfig.features.useMockServer
? handleMock(() => uploadInsuranceDocument(payload), 420)
: handleRequest(async () => {
const response = await apiClient.post<InsuranceDocument>(
endpoints.insurance.uploadDocument,
payload,
);
return response.data;
}),
invalidatesTags: ['Insurance'],
}),
getInsuranceNotifications: builder.query<InsuranceNotification[], void>({
queryFn: async () =>
appConfig.features.useMockServer
? handleMock(() => listInsuranceNotifications(), 180)
: handleRequest(async () => {
const response = await apiClient.get<InsuranceNotification[]>(
endpoints.insurance.notifications,
);
return response.data;
}),
providesTags: ['Insurance'],
}),
markInsuranceNotificationRead: builder.mutation<{ success: boolean }, string>({
queryFn: async (notificationId) =>
appConfig.features.useMockServer
? handleMock(() => markInsuranceNotificationRead(notificationId), 120)
: handleRequest(async () => {
const response = await apiClient.post<{ success: boolean }>(
endpoints.insurance.markNotificationRead(notificationId),
);
return response.data;
}),
invalidatesTags: ['Insurance'],
}),
}),
});
export const {
useCompareInsurancePlansQuery,
useEstimateInsurancePremiumQuery,
useGetInsuranceClaimsQuery,
useGetInsuranceDashboardQuery,
useGetInsuranceDocumentsQuery,
useGetInsuranceNotificationsQuery,
useGetInsurancePlansQuery,
useGetInsurancePoliciesQuery,
useGetInsurancePolicyDetailQuery,
useGetInsurancePremiumDuesQuery,
useMarkInsuranceNotificationReadMutation,
usePayInsurancePremiumMutation,
usePurchaseInsurancePolicyMutation,
useRequestInsuranceOtpMutation,
useSubmitInsuranceClaimMutation,
useToggleInsurancePolicyAutoRenewMutation,
useUploadInsuranceDocumentMutation,
} = insuranceApi;
@@ -0,0 +1,928 @@
import type {
InsuranceCategory,
InsuranceClaim,
InsuranceDashboardPayload,
InsuranceDocument,
InsuranceInsurer,
InsuranceNotification,
InsuranceOtpSession,
InsurancePlan,
InsurancePolicy,
InsurancePremiumDue,
PayInsurancePremiumPayload,
PayInsurancePremiumResult,
PurchaseInsurancePayload,
PurchaseInsuranceResult,
RequestInsuranceOtpPayload,
SubmitInsuranceClaimPayload,
SubmitInsuranceClaimResult,
UploadInsuranceDocumentPayload,
} from '../types';
import {
addYears,
getEarliestPremiumDue,
getNextDateForFrequency,
getPolicyStatusFromDueDate,
getPolicyTotalCoverage,
getPremiumLateFee,
} from '../utils/insuranceMath';
const cloneValue = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
const insurers: InsuranceInsurer[] = [
{
id: 'ins_guardian',
name: 'Guardian Health Assurance',
claimSettlementRatio: 96.4,
supportPhone: '1800-210-8899',
supportEmail: 'care@guardianhealth.co.in',
website: 'https://guardianhealth.example',
address: 'Koramangala 5th Block, Bengaluru',
},
{
id: 'ins_zenith',
name: 'Zenith Life Assurance',
claimSettlementRatio: 98.2,
supportPhone: '1800-450-2100',
supportEmail: 'support@zenithlife.co.in',
website: 'https://zenithlife.example',
address: 'BKC, Mumbai',
},
{
id: 'ins_drivesure',
name: 'DriveSure General Insurance',
claimSettlementRatio: 94.9,
supportPhone: '1800-120-7788',
supportEmail: 'claims@drivesure.co.in',
website: 'https://drivesure.example',
address: 'Madhapur, Hyderabad',
},
{
id: 'ins_globetrot',
name: 'GlobeTrot Protect',
claimSettlementRatio: 93.6,
supportPhone: '1800-510-3344',
supportEmail: 'travel@globetrotprotect.co.in',
website: 'https://globetrotprotect.example',
address: 'Viman Nagar, Pune',
},
{
id: 'ins_safenest',
name: 'SafeNest General Insurance',
claimSettlementRatio: 95.1,
supportPhone: '1800-880-4455',
supportEmail: 'hello@safenest.co.in',
website: 'https://safenest.example',
address: 'T Nagar, Chennai',
},
];
const planCatalog: InsurancePlan[] = [
{
id: 'health_guard_plus',
category: 'Health Insurance',
insurerId: 'ins_guardian',
insurerName: 'Guardian Health Assurance',
name: 'Guardian Family Shield',
premium: 2140,
premiumFrequency: 'Monthly',
coverAmount: 1500000,
tenureYears: 1,
summary: 'Family floater plan with cashless hospitalization and annual wellness checks.',
benefits: ['Cashless hospitalization', 'No-claim bonus', 'Day-care coverage'],
inclusions: ['Room rent without cap', 'Annual preventive check-up', 'Ambulance cover'],
exclusions: ['Cosmetic procedures', 'Self-inflicted injuries', 'Waiting period ailments'],
claimSettlementRatio: 96.4,
waitingPeriodDays: 30,
recommendedFor: 'Young families seeking cashless hospitalization cover.',
addOns: ['Maternity add-on', 'Critical illness rider'],
},
{
id: 'health_elite_family',
category: 'Health Insurance',
insurerId: 'ins_guardian',
insurerName: 'Guardian Health Assurance',
name: 'Guardian Elite Care',
premium: 3560,
premiumFrequency: 'Monthly',
coverAmount: 2500000,
tenureYears: 1,
summary: 'Higher family cover with OPD benefits and annual wellness rewards.',
benefits: ['Global emergency support', 'OPD allowance', 'Restoration benefit'],
inclusions: ['Cashless hospital network', 'Second opinion', 'Tele-consult access'],
exclusions: ['Non-medical consumables', 'Experimental treatment'],
claimSettlementRatio: 96.4,
waitingPeriodDays: 15,
recommendedFor: 'Families wanting higher hospitalization cover and OPD support.',
addOns: ['Unlimited restoration', 'Room upgrade'],
},
{
id: 'life_secure_term',
category: 'Life Insurance',
insurerId: 'ins_zenith',
insurerName: 'Zenith Life Assurance',
name: 'Zenith Secure Term',
premium: 1890,
premiumFrequency: 'Monthly',
coverAmount: 10000000,
tenureYears: 20,
summary: 'Pure term cover with flexible payout and wellness-linked discounts.',
benefits: ['High sum assured', 'Terminal illness benefit', 'Payout flexibility'],
inclusions: ['Monthly income option', 'Accidental death rider option', 'Digital issuance'],
exclusions: ['Suicide within first year', 'Material misstatement'],
claimSettlementRatio: 98.2,
recommendedFor: 'Primary earners seeking long-duration family income protection.',
addOns: ['Waiver of premium', 'Accidental disability rider'],
},
{
id: 'life_income_shield',
category: 'Life Insurance',
insurerId: 'ins_zenith',
insurerName: 'Zenith Life Assurance',
name: 'Zenith Income Shield',
premium: 2750,
premiumFrequency: 'Monthly',
coverAmount: 15000000,
tenureYears: 25,
summary: 'Term protection with staggered monthly family income support.',
benefits: ['Monthly income payout', 'Return of premium option', 'Life-stage upgrades'],
inclusions: ['Instant policy issuance', 'Wellness discount'],
exclusions: ['Suicide exclusion clause', 'Fraudulent disclosure'],
claimSettlementRatio: 98.2,
recommendedFor: 'Households that prefer monthly income support for nominees.',
addOns: ['Critical illness', 'Child income support'],
},
{
id: 'motor_car_smart',
category: 'Motor Insurance',
insurerId: 'ins_drivesure',
insurerName: 'DriveSure General Insurance',
name: 'DriveSure Car Secure',
premium: 12450,
premiumFrequency: 'Yearly',
coverAmount: 800000,
tenureYears: 1,
summary: 'Comprehensive car cover with zero-depreciation and roadside assistance.',
benefits: ['Zero depreciation', 'Roadside assistance', 'Cashless garage network'],
inclusions: ['Own damage cover', 'Third-party liability', 'Engine protection'],
exclusions: ['Driving without license', 'Mechanical breakdown', 'War risks'],
claimSettlementRatio: 94.9,
deductibleAmount: 1000,
recommendedFor: 'Private car owners who want broad repair and liability cover.',
addOns: ['Consumables cover', 'Return to invoice'],
},
{
id: 'motor_bike_smart',
category: 'Motor Insurance',
insurerId: 'ins_drivesure',
insurerName: 'DriveSure General Insurance',
name: 'DriveSure Bike Protect',
premium: 4680,
premiumFrequency: 'Yearly',
coverAmount: 250000,
tenureYears: 1,
summary: 'Comprehensive bike cover with theft assistance and third-party protection.',
benefits: ['Theft assistance', 'Garage network', 'Personal accident cover'],
inclusions: ['Own damage cover', 'Third-party liability', '24x7 claims'],
exclusions: ['Driving under influence', 'Commercial misuse'],
claimSettlementRatio: 94.9,
deductibleAmount: 500,
recommendedFor: 'Bike owners seeking low-friction annual protection.',
addOns: ['Return to invoice', 'Roadside assistance'],
},
{
id: 'travel_global_trip',
category: 'Travel Insurance',
insurerId: 'ins_globetrot',
insurerName: 'GlobeTrot Protect',
name: 'Global Trip Shield',
premium: 1450,
premiumFrequency: 'Yearly',
coverAmount: 5000000,
tenureYears: 1,
summary: 'Multi-trip cover for medical emergencies, delays, and baggage loss.',
benefits: ['Medical emergency cover', 'Trip delay assistance', 'Baggage loss support'],
inclusions: ['Emergency evacuation', 'Passport loss cover', 'Adventure sports add-on'],
exclusions: ['High-risk countries', 'Pre-existing conditions', 'Illegal acts'],
claimSettlementRatio: 93.6,
recommendedFor: 'Frequent travellers who need annual international trip protection.',
addOns: ['Golf cover', 'Home burglary cover'],
},
{
id: 'travel_student_explore',
category: 'Travel Insurance',
insurerId: 'ins_globetrot',
insurerName: 'GlobeTrot Protect',
name: 'Student Explore',
premium: 980,
premiumFrequency: 'Yearly',
coverAmount: 2000000,
tenureYears: 1,
summary: 'Student travel cover for tuition emergencies and health incidents abroad.',
benefits: ['Sponsor protection', 'Study interruption cover', 'Medical emergencies'],
inclusions: ['Loss of baggage', 'Compassionate visit', 'Laptop cover'],
exclusions: ['Pre-existing ailments', 'Professional sports'],
claimSettlementRatio: 93.6,
recommendedFor: 'Students travelling overseas for study programs.',
addOns: ['Flight cancellation', 'Device cover'],
},
{
id: 'property_home_secure',
category: 'Property Insurance',
insurerId: 'ins_safenest',
insurerName: 'SafeNest General Insurance',
name: 'SafeNest Home Secure',
premium: 5920,
premiumFrequency: 'Yearly',
coverAmount: 4500000,
tenureYears: 1,
summary: 'Structure and contents protection with burglary and fire cover.',
benefits: ['Structure protection', 'Contents cover', 'Burglary assistance'],
inclusions: ['Fire and allied perils', 'Natural calamity cover', 'Electrical equipment cover'],
exclusions: ['Wear and tear', 'Intentional damage', 'Vacant property losses'],
claimSettlementRatio: 95.1,
deductibleAmount: 2500,
recommendedFor: 'Homeowners safeguarding structure and interior contents.',
addOns: ['Jewellery cover', 'Tenant liability'],
},
{
id: 'property_landlord_plus',
category: 'Property Insurance',
insurerId: 'ins_safenest',
insurerName: 'SafeNest General Insurance',
name: 'SafeNest Landlord Plus',
premium: 7380,
premiumFrequency: 'Yearly',
coverAmount: 7000000,
tenureYears: 1,
summary: 'Landlord-friendly cover with rental income disruption and liability support.',
benefits: ['Rental disruption cover', 'Liability cover', 'Burglary support'],
inclusions: ['Fire cover', 'Flood cover', 'Owner liability'],
exclusions: ['Tenant negligence without evidence', 'Construction defects'],
claimSettlementRatio: 95.1,
deductibleAmount: 3500,
recommendedFor: 'Landlords with higher-value rental properties.',
addOns: ['Loss of rent', 'Glass breakage'],
},
];
const policySeed: InsurancePolicy[] = [
{
id: 'policy_health_001',
planId: 'health_guard_plus',
planName: 'Guardian Family Shield',
category: 'Health Insurance',
insurer: insurers[0]!,
policyNumber: 'HLT-2026-441208',
insuredName: 'Aarav Sharma',
premiumAmount: 2140,
premiumFrequency: 'Monthly',
coverageAmount: 1500000,
status: 'Active',
startDate: '2026-01-03T00:00:00.000Z',
expiryDate: '2027-01-02T00:00:00.000Z',
nextPremiumDueDate: '2026-05-02T00:00:00.000Z',
benefits: ['Cashless hospitalization', 'No-claim bonus', 'Day-care coverage'],
inclusions: ['Room rent without cap', 'Annual preventive check-up', 'Ambulance cover'],
exclusions: ['Cosmetic procedures', 'Self-inflicted injuries', 'Waiting period ailments'],
nomineeName: 'Megha Sharma',
nomineeRelationship: 'Spouse',
autoRenewEnabled: true,
membersCovered: 3,
healthDetails: {
familyMembers: 3,
medicalHistory: 'None',
smoker: 'No',
},
documents: [
{
id: 'policy_doc_health_001',
name: 'Policy Schedule',
mimeType: 'application/pdf',
uploadedAt: '2026-01-03T09:00:00.000Z',
url: 'https://mock.bankcbs.dev/insurance/policies/HLT-2026-441208.pdf',
sizeKb: 612,
},
],
},
{
id: 'policy_motor_001',
planId: 'motor_car_smart',
planName: 'DriveSure Car Secure',
category: 'Motor Insurance',
insurer: insurers[2]!,
policyNumber: 'MTR-2025-778231',
insuredName: 'Aarav Sharma',
premiumAmount: 12450,
premiumFrequency: 'Yearly',
coverageAmount: 800000,
status: 'Renewal Due',
startDate: '2025-04-28T00:00:00.000Z',
expiryDate: '2026-04-27T00:00:00.000Z',
nextPremiumDueDate: '2026-04-27T00:00:00.000Z',
benefits: ['Zero depreciation', 'Roadside assistance', 'Cashless garage network'],
inclusions: ['Own damage cover', 'Third-party liability', 'Engine protection'],
exclusions: ['Driving without license', 'Mechanical breakdown', 'War risks'],
nomineeName: 'Megha Sharma',
nomineeRelationship: 'Spouse',
autoRenewEnabled: false,
membersCovered: 1,
deductibleAmount: 1000,
vehicleDetails: {
vehicleType: 'Car',
makeModel: 'Honda City ZX',
registrationNumber: 'KA01MN4421',
registrationYear: '2023',
},
documents: [
{
id: 'policy_doc_motor_001',
name: 'Policy Schedule',
mimeType: 'application/pdf',
uploadedAt: '2025-04-28T10:15:00.000Z',
url: 'https://mock.bankcbs.dev/insurance/policies/MTR-2025-778231.pdf',
sizeKb: 544,
},
],
},
{
id: 'policy_life_001',
planId: 'life_secure_term',
planName: 'Zenith Secure Term',
category: 'Life Insurance',
insurer: insurers[1]!,
policyNumber: 'LIF-2025-125504',
insuredName: 'Aarav Sharma',
premiumAmount: 1890,
premiumFrequency: 'Monthly',
coverageAmount: 10000000,
status: 'Active',
startDate: '2025-09-18T00:00:00.000Z',
expiryDate: '2045-09-17T00:00:00.000Z',
nextPremiumDueDate: '2026-04-18T00:00:00.000Z',
benefits: ['High sum assured', 'Terminal illness benefit', 'Payout flexibility'],
inclusions: ['Monthly income option', 'Accidental death rider option', 'Digital issuance'],
exclusions: ['Suicide within first year', 'Material misstatement'],
nomineeName: 'Megha Sharma',
nomineeRelationship: 'Spouse',
autoRenewEnabled: true,
membersCovered: 1,
lifeDetails: {
annualIncome: 1800000,
occupation: 'Product Manager',
smoker: 'No',
},
documents: [
{
id: 'policy_doc_life_001',
name: 'Policy Certificate',
mimeType: 'application/pdf',
uploadedAt: '2025-09-18T08:45:00.000Z',
url: 'https://mock.bankcbs.dev/insurance/policies/LIF-2025-125504.pdf',
sizeKb: 488,
},
],
},
];
const documentSeed: InsuranceDocument[] = [
{
id: 'ins_doc_kyc_001',
name: 'Aadhaar Card',
type: 'Aadhaar',
category: 'kyc',
status: 'verified',
uploadedAt: '2026-01-02T07:40:00.000Z',
mimeType: 'application/pdf',
url: 'https://mock.bankcbs.dev/insurance/documents/aadhaar.pdf',
sizeKb: 214,
},
{
id: 'ins_doc_kyc_002',
name: 'PAN Card',
type: 'PAN',
category: 'kyc',
status: 'verified',
uploadedAt: '2026-01-02T07:41:00.000Z',
mimeType: 'image/jpeg',
url: 'https://mock.bankcbs.dev/insurance/documents/pan.jpg',
sizeKb: 188,
},
{
id: 'ins_doc_claim_001',
name: 'Hospital Bill',
type: 'Invoice',
category: 'claim',
status: 'uploaded',
uploadedAt: '2026-04-07T12:15:00.000Z',
mimeType: 'application/pdf',
url: 'https://mock.bankcbs.dev/insurance/documents/hospital_bill.pdf',
sizeKb: 742,
linkedClaimId: 'claim_health_001',
},
{
id: 'ins_doc_claim_002',
name: 'Discharge Summary',
type: 'Medical Report',
category: 'claim',
status: 'uploaded',
uploadedAt: '2026-04-07T12:20:00.000Z',
mimeType: 'application/pdf',
url: 'https://mock.bankcbs.dev/insurance/documents/discharge_summary.pdf',
sizeKb: 580,
linkedClaimId: 'claim_health_001',
},
{
id: 'ins_doc_claim_003',
name: 'Workshop Invoice',
type: 'Invoice',
category: 'claim',
status: 'verified',
uploadedAt: '2026-01-21T14:20:00.000Z',
mimeType: 'application/pdf',
url: 'https://mock.bankcbs.dev/insurance/documents/workshop_invoice.pdf',
sizeKb: 436,
linkedClaimId: 'claim_motor_001',
},
];
const claimSeed: InsuranceClaim[] = [
{
id: 'claim_health_001',
claimNumber: 'CLM-H-2026-0931',
policyId: 'policy_health_001',
policyNumber: 'HLT-2026-441208',
policyName: 'Guardian Family Shield',
category: 'Health Insurance',
reason: 'Hospitalization due to dengue',
eventDate: '2026-04-05T00:00:00.000Z',
amount: 85000,
status: 'Under Review',
createdAt: '2026-04-07T11:55:00.000Z',
updatedAt: '2026-04-09T10:20:00.000Z',
notes: 'Cashless request received. Hospital estimate and discharge papers under review.',
documents: documentSeed.filter((item) => item.linkedClaimId === 'claim_health_001'),
},
{
id: 'claim_motor_001',
claimNumber: 'CLM-M-2026-0107',
policyId: 'policy_motor_001',
policyNumber: 'MTR-2025-778231',
policyName: 'DriveSure Car Secure',
category: 'Motor Insurance',
reason: 'Rear bumper repair after low-speed collision',
eventDate: '2026-01-18T00:00:00.000Z',
amount: 32000,
status: 'Settled',
createdAt: '2026-01-19T09:10:00.000Z',
updatedAt: '2026-01-24T16:10:00.000Z',
notes: 'Claim settled to partnered cashless garage.',
documents: documentSeed.filter((item) => item.linkedClaimId === 'claim_motor_001'),
},
];
const notificationSeed: InsuranceNotification[] = [
{
id: 'ins_notif_001',
type: 'premium_due',
title: 'Premium due in 5 days',
body: 'Zenith Secure Term premium of Rs 1,890 is due on 18 Apr 2026.',
createdAt: '2026-04-13T06:00:00.000Z',
read: false,
policyId: 'policy_life_001',
},
{
id: 'ins_notif_002',
type: 'policy_expiry',
title: 'Motor policy expires soon',
body: 'DriveSure Car Secure expires on 27 Apr 2026. Renew to avoid a break in cover.',
createdAt: '2026-04-12T10:10:00.000Z',
read: false,
policyId: 'policy_motor_001',
},
{
id: 'ins_notif_003',
type: 'claim_update',
title: 'Claim moved to review',
body: 'Your health claim CLM-H-2026-0931 is now under review.',
createdAt: '2026-04-09T10:20:00.000Z',
read: true,
claimId: 'claim_health_001',
policyId: 'policy_health_001',
},
{
id: 'ins_notif_004',
type: 'offer',
title: 'Upgrade with critical illness rider',
body: 'You can add a critical illness rider to your life cover in under two minutes.',
createdAt: '2026-04-08T07:45:00.000Z',
read: true,
policyId: 'policy_life_001',
},
];
let mockPlans = cloneValue(planCatalog);
let mockPolicies = cloneValue(policySeed);
let mockDocuments = cloneValue(documentSeed);
let mockClaims = cloneValue(claimSeed);
let mockNotifications = cloneValue(notificationSeed);
const findInsurer = (id: string) => insurers.find((insurer) => insurer.id === id) ?? insurers[0]!;
const getPolicyDocuments = (policyNumber: string): InsurancePolicy['documents'] => [
{
id: `policy_doc_${policyNumber}`,
name: 'Policy Schedule',
mimeType: 'application/pdf',
uploadedAt: new Date().toISOString(),
url: `https://mock.bankcbs.dev/insurance/policies/${policyNumber}.pdf`,
sizeKb: 624,
},
];
const withStatus = (policy: InsurancePolicy): InsurancePolicy => ({
...policy,
status: getPolicyStatusFromDueDate(policy.nextPremiumDueDate, policy.expiryDate),
});
const getPremiumDues = (): InsurancePremiumDue[] =>
mockPolicies
.map((policy) => ({
id: `due_${policy.id}`,
policyId: policy.id,
planName: policy.planName,
policyNumber: policy.policyNumber,
amount: policy.premiumAmount + getPremiumLateFee(policy.nextPremiumDueDate, policy.premiumAmount),
dueDate: policy.nextPremiumDueDate,
frequency: policy.premiumFrequency,
lateFee: getPremiumLateFee(policy.nextPremiumDueDate, policy.premiumAmount),
autoRenewEnabled: policy.autoRenewEnabled,
}))
.sort(
(left, right) => new Date(left.dueDate).getTime() - new Date(right.dueDate).getTime(),
);
const buildDashboard = (): InsuranceDashboardPayload => {
const policies = mockPolicies.map(withStatus);
const duePremiums = getPremiumDues();
return {
policies,
totalCoverageAmount: getPolicyTotalCoverage(
policies.filter((policy) => policy.status !== 'Expired'),
),
activePoliciesCount: policies.filter((policy) => policy.status !== 'Expired').length,
duePremiums,
upcomingPremium: getEarliestPremiumDue(duePremiums),
recentClaims: [...mockClaims]
.sort(
(left, right) =>
new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime(),
)
.slice(0, 3)
.map((claim) => ({ ...claim, documents: cloneValue(claim.documents) })),
notifications: [...mockNotifications]
.sort(
(left, right) =>
new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime(),
)
.slice(0, 4)
.map((item) => ({ ...item })),
};
};
export const seedInsurancePlans = () => cloneValue(planCatalog);
export const listInsurancePlans = (category?: InsuranceCategory) =>
cloneValue(category ? mockPlans.filter((plan) => plan.category === category) : mockPlans);
export const compareInsurancePlans = (planIds: string[]) =>
cloneValue(mockPlans.filter((plan) => planIds.includes(plan.id)));
export const listInsurancePolicies = () =>
cloneValue(
mockPolicies
.map(withStatus)
.sort(
(left, right) =>
new Date(left.nextPremiumDueDate).getTime() - new Date(right.nextPremiumDueDate).getTime(),
),
);
export const getInsurancePolicyDetail = (policyId: string) => {
const policy = mockPolicies.find((item) => item.id === policyId);
if (!policy) {
throw new Error('Policy not found.');
}
return cloneValue(withStatus(policy));
};
export const getInsuranceDashboard = () => cloneValue(buildDashboard());
export const listInsuranceClaims = (policyId?: string) =>
cloneValue(
(policyId ? mockClaims.filter((claim) => claim.policyId === policyId) : mockClaims).sort(
(left, right) =>
new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime(),
),
);
export const listInsuranceDocuments = (category?: 'kyc' | 'claim') =>
cloneValue(category ? mockDocuments.filter((document) => document.category === category) : mockDocuments);
export const listInsuranceNotifications = () =>
cloneValue(
[...mockNotifications].sort(
(left, right) =>
new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime(),
),
);
export const requestInsuranceOtp = (
payload: RequestInsuranceOtpPayload,
): InsuranceOtpSession => ({
referenceId: `${payload.action.toUpperCase()}_${Date.now()}`,
destinationMask: '+91 98*** 3210',
mockCodeHint: '123456',
});
const assertOtp = (otp: string) => {
if (otp !== '123456') {
throw new Error('The OTP you entered is invalid.');
}
};
export const purchaseInsurancePolicy = (
payload: PurchaseInsurancePayload,
): PurchaseInsuranceResult => {
assertOtp(payload.otp);
const selectedPlan = mockPlans.find((plan) => plan.id === payload.draft.selectedPlanId);
if (!selectedPlan) {
throw new Error('Select an insurance plan to continue.');
}
const now = new Date().toISOString();
const policyId = `policy_${Date.now()}`;
const policyNumber = `INS-${new Date().getFullYear()}-${Math.floor(100000 + Math.random() * 900000)}`;
const nextPremiumDueDate = getNextDateForFrequency(now, selectedPlan.premiumFrequency);
const expiryDate =
selectedPlan.category === 'Life Insurance'
? addYears(now, selectedPlan.tenureYears)
: addYears(now, 1);
const policy: InsurancePolicy = {
id: policyId,
planId: selectedPlan.id,
planName: selectedPlan.name,
category: selectedPlan.category,
insurer: findInsurer(selectedPlan.insurerId),
policyNumber,
insuredName: payload.draft.personal.fullName,
premiumAmount: selectedPlan.premium,
premiumFrequency: selectedPlan.premiumFrequency,
coverageAmount: selectedPlan.coverAmount,
status: 'Active',
startDate: now,
expiryDate,
nextPremiumDueDate,
benefits: [...selectedPlan.benefits],
inclusions: [...selectedPlan.inclusions],
exclusions: [...selectedPlan.exclusions],
nomineeName: payload.draft.nominee.name,
nomineeRelationship: payload.draft.nominee.relationship,
autoRenewEnabled: payload.draft.autoRenewEnabled,
membersCovered: Number(payload.draft.specificDetails.familyMembers || 1),
deductibleAmount: selectedPlan.deductibleAmount,
documents: getPolicyDocuments(policyNumber),
};
if (selectedPlan.category === 'Health Insurance') {
policy.healthDetails = {
familyMembers: Number(payload.draft.specificDetails.familyMembers || 1),
medicalHistory: payload.draft.specificDetails.medicalHistory || 'None',
smoker: payload.draft.specificDetails.smoker || 'No',
};
}
if (selectedPlan.category === 'Life Insurance') {
policy.lifeDetails = {
annualIncome: Number(payload.draft.specificDetails.annualIncome || 0),
occupation: payload.draft.specificDetails.occupation || '',
smoker: payload.draft.specificDetails.smoker || 'No',
};
}
if (selectedPlan.category === 'Motor Insurance') {
policy.vehicleDetails = {
vehicleType: payload.draft.specificDetails.vehicleType || '',
makeModel: payload.draft.specificDetails.makeModel || '',
registrationNumber: payload.draft.specificDetails.registrationNumber || '',
registrationYear: payload.draft.specificDetails.registrationYear || '',
};
}
if (selectedPlan.category === 'Travel Insurance') {
policy.travelDetails = {
destination: payload.draft.specificDetails.destination || '',
departureDate: payload.draft.specificDetails.departureDate || '',
returnDate: payload.draft.specificDetails.returnDate || '',
travellers: Number(payload.draft.specificDetails.travellers || 1),
passportNumber: payload.draft.specificDetails.passportNumber || '',
};
}
if (selectedPlan.category === 'Property Insurance') {
policy.propertyDetails = {
propertyType: payload.draft.specificDetails.propertyType || '',
propertyValue: Number(payload.draft.specificDetails.propertyValue || 0),
constructionYear: payload.draft.specificDetails.constructionYear || '',
propertyAddress: payload.draft.specificDetails.propertyAddress || '',
};
}
mockPolicies = [policy, ...mockPolicies];
mockNotifications = [
{
id: `ins_notif_${Date.now()}`,
type: 'offer',
title: 'Policy issued successfully',
body: `${selectedPlan.name} has been issued with policy number ${policyNumber}.`,
createdAt: now,
read: false,
policyId,
},
...mockNotifications,
];
return {
policy: cloneValue(policy),
transactionRef: `TXN${Date.now()}`,
};
};
export const payInsurancePremium = (
payload: PayInsurancePremiumPayload,
): PayInsurancePremiumResult => {
assertOtp(payload.otp);
const policyIndex = mockPolicies.findIndex((policy) => policy.id === payload.policyId);
if (policyIndex < 0) {
throw new Error('Policy not found.');
}
const current = mockPolicies[policyIndex]!;
const nextDueDate = getNextDateForFrequency(
current.nextPremiumDueDate,
current.premiumFrequency,
);
const shouldExtendExpiry =
Math.abs(
new Date(current.expiryDate).getTime() - new Date(current.nextPremiumDueDate).getTime(),
) <= 24 * 60 * 60 * 1000;
const updatedPolicy: InsurancePolicy = {
...current,
nextPremiumDueDate: nextDueDate,
expiryDate: shouldExtendExpiry ? addYears(current.expiryDate, 1) : current.expiryDate,
status: 'Active',
};
mockPolicies = mockPolicies.map((policy, index) =>
index === policyIndex ? updatedPolicy : policy,
);
mockNotifications = [
{
id: `ins_notif_${Date.now()}`,
type: 'premium_due',
title: 'Premium paid successfully',
body: `${updatedPolicy.planName} premium of Rs ${payload.amount.toLocaleString('en-IN')} paid successfully.`,
createdAt: new Date().toISOString(),
read: false,
policyId: updatedPolicy.id,
},
...mockNotifications,
];
return {
success: true,
transactionRef: `TXN${Date.now()}`,
nextDueDate,
};
};
export const toggleInsurancePolicyAutoRenew = (policyId: string, enabled: boolean) => {
const policyIndex = mockPolicies.findIndex((policy) => policy.id === policyId);
if (policyIndex < 0) {
throw new Error('Policy not found.');
}
const updatedPolicy = { ...mockPolicies[policyIndex]!, autoRenewEnabled: enabled };
mockPolicies = mockPolicies.map((policy, index) =>
index === policyIndex ? updatedPolicy : policy,
);
return cloneValue(updatedPolicy);
};
export const submitInsuranceClaim = (
payload: SubmitInsuranceClaimPayload,
): SubmitInsuranceClaimResult => {
assertOtp(payload.otp);
const policy = mockPolicies.find((item) => item.id === payload.policyId);
if (!policy) {
throw new Error('Choose a valid policy for the claim.');
}
const claimDocuments = mockDocuments.filter((document) =>
payload.documentIds.includes(document.id),
);
const claimId = `claim_${Date.now()}`;
const claimNumber = `CLM-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`;
const now = new Date().toISOString();
const nextClaim: InsuranceClaim = {
id: claimId,
claimNumber,
policyId: policy.id,
policyNumber: policy.policyNumber,
policyName: policy.planName,
category: policy.category,
reason: payload.reason,
eventDate: payload.eventDate,
amount: payload.amount,
status: 'Submitted',
createdAt: now,
updatedAt: now,
notes: payload.description,
documents: claimDocuments.map((document) => ({
...document,
linkedClaimId: claimId,
})),
};
mockClaims = [nextClaim, ...mockClaims];
mockDocuments = mockDocuments.map((document) =>
payload.documentIds.includes(document.id)
? { ...document, linkedClaimId: claimId }
: document,
);
mockNotifications = [
{
id: `ins_notif_${Date.now()}`,
type: 'claim_update',
title: 'Claim submitted',
body: `${claimNumber} has been lodged and shared with ${policy.insurer.name}.`,
createdAt: now,
read: false,
policyId: policy.id,
claimId,
},
...mockNotifications,
];
return {
claim: cloneValue(nextClaim),
referenceId: claimNumber,
};
};
export const uploadInsuranceDocument = (payload: UploadInsuranceDocumentPayload) => {
if (payload.existingDocumentId) {
mockDocuments = mockDocuments.filter((document) => document.id !== payload.existingDocumentId);
}
const document: InsuranceDocument = {
id: `ins_doc_${Date.now()}`,
name: payload.name,
type: payload.type,
category: payload.category,
status:
payload.category === 'kyc' && (payload.type === 'Aadhaar' || payload.type === 'PAN')
? 'verified'
: 'uploaded',
uploadedAt: new Date().toISOString(),
mimeType: payload.mimeType,
url: payload.uri,
sizeKb: payload.sizeKb,
linkedClaimId: payload.linkedClaimId,
};
mockDocuments = [document, ...mockDocuments];
return cloneValue(document);
};
export const markInsuranceNotificationRead = (notificationId: string) => {
mockNotifications = mockNotifications.map((notification) =>
notification.id === notificationId ? { ...notification, read: true } : notification,
);
return { success: true };
};
@@ -0,0 +1,12 @@
import type { RootState } from '../../../app/store';
export const selectInsuranceState = (state: RootState) => state.insurance;
export const selectInsurancePlans = (state: RootState) => state.insurance.plans;
export const selectInsuranceDraft = (state: RootState) => state.insurance.draft;
export const selectInsurancePurchaseStep = (state: RootState) => state.insurance.purchaseStep;
export const selectInsuranceCalculator = (state: RootState) => state.insurance.calculator;
export const selectInsuranceCategory = (state: RootState) => state.insurance.draft.category;
export const selectInsuranceComparedPlanIds = (state: RootState) =>
state.insurance.draft.comparePlanIds;
export const selectInsuranceSelectedPlanId = (state: RootState) =>
state.insurance.draft.selectedPlanId;
@@ -0,0 +1,160 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { AsyncStatus } from '../../../types/app';
import type {
InsuranceAddressDetails,
InsuranceCalculatorInput,
InsuranceCategory,
InsuranceKycDetails,
InsuranceNomineeDetails,
InsurancePaymentMethod,
InsurancePersonalDetails,
InsurancePlan,
InsurancePurchaseDraft,
InsurancePurchaseStep,
} from '../types';
import { seedInsurancePlans } from './insuranceThunk';
interface InsuranceState {
status: AsyncStatus;
plans: InsurancePlan[];
purchaseStep: InsurancePurchaseStep;
draft: InsurancePurchaseDraft;
calculator: InsuranceCalculatorInput;
}
const createInitialDraft = (
category: InsuranceCategory = 'Health Insurance',
): InsurancePurchaseDraft => ({
category,
personal: {
fullName: 'Aarav Sharma',
age: '31',
gender: 'Male',
email: 'aarav.sharma@bankcbs.dev',
phone: '9876543210',
},
nominee: {
name: 'Megha Sharma',
relationship: 'Spouse',
age: '29',
},
address: {
line1: '18, 7th Main, Koramangala',
city: 'Bengaluru',
state: 'Karnataka',
postalCode: '560095',
},
kyc: {
aadhaarNumber: '123412341234',
panNumber: 'ABCDE1234F',
},
specificDetails: {},
selectedPlanId: null,
paymentMethod: 'UPI',
autoRenewEnabled: true,
comparePlanIds: [],
});
const initialState: InsuranceState = {
status: 'idle',
plans: [],
purchaseStep: 'details',
draft: createInitialDraft(),
calculator: {
category: 'Health Insurance',
age: '31',
coverageAmount: '1500000',
tenureYears: '1',
},
};
const insuranceSlice = createSlice({
name: 'insurance',
initialState,
reducers: {
setInsuranceCategory(state, action: PayloadAction<InsuranceCategory>) {
state.draft = createInitialDraft(action.payload);
state.purchaseStep = 'details';
state.calculator.category = action.payload;
},
setInsurancePurchaseStep(state, action: PayloadAction<InsurancePurchaseStep>) {
state.purchaseStep = action.payload;
},
updateInsurancePersonal(
state,
action: PayloadAction<{ field: keyof InsurancePersonalDetails; value: string }>,
) {
state.draft.personal[action.payload.field] = action.payload.value as never;
},
updateInsuranceNominee(
state,
action: PayloadAction<{ field: keyof InsuranceNomineeDetails; value: string }>,
) {
state.draft.nominee[action.payload.field] = action.payload.value;
},
updateInsuranceAddress(
state,
action: PayloadAction<{ field: keyof InsuranceAddressDetails; value: string }>,
) {
state.draft.address[action.payload.field] = action.payload.value;
},
updateInsuranceKyc(
state,
action: PayloadAction<{ field: keyof InsuranceKycDetails; value: string }>,
) {
state.draft.kyc[action.payload.field] = action.payload.value;
},
updateInsuranceSpecificDetail(
state,
action: PayloadAction<{ key: string; value: string }>,
) {
state.draft.specificDetails[action.payload.key] = action.payload.value;
},
setInsuranceSelectedPlan(state, action: PayloadAction<string | null>) {
state.draft.selectedPlanId = action.payload;
},
toggleInsuranceComparedPlan(state, action: PayloadAction<string>) {
if (state.draft.comparePlanIds.includes(action.payload)) {
state.draft.comparePlanIds = state.draft.comparePlanIds.filter(
(id) => id !== action.payload,
);
return;
}
state.draft.comparePlanIds = [...state.draft.comparePlanIds, action.payload].slice(-3);
},
setInsurancePaymentMethod(state, action: PayloadAction<InsurancePaymentMethod>) {
state.draft.paymentMethod = action.payload;
},
setInsuranceAutoRenew(state, action: PayloadAction<boolean>) {
state.draft.autoRenewEnabled = action.payload;
},
resetInsuranceDraft(state) {
state.purchaseStep = 'details';
state.draft = createInitialDraft(state.draft.category);
},
updateInsuranceCalculator(
state,
action: PayloadAction<{ field: keyof InsuranceCalculatorInput; value: string }>,
) {
state.calculator[action.payload.field] = action.payload.value as never;
},
},
extraReducers: (builder) => {
builder
.addCase(seedInsurancePlans.pending, (state) => {
state.status = 'loading';
})
.addCase(seedInsurancePlans.fulfilled, (state, action) => {
state.status = 'succeeded';
state.plans = action.payload;
})
.addCase(seedInsurancePlans.rejected, (state) => {
state.status = 'failed';
});
},
});
export const insuranceActions = insuranceSlice.actions;
export const insuranceReducer = insuranceSlice.reducer;
@@ -0,0 +1,13 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { sleep } from '../../../utils/async';
import type { InsurancePlan } from '../types';
import { seedInsurancePlanCatalog } from '../services/insuranceApi';
export const seedInsurancePlans = createAsyncThunk<InsurancePlan[]>(
'insurance/seedPlans',
async () => {
await sleep(140);
return seedInsurancePlanCatalog();
},
);
+515
View File
@@ -0,0 +1,515 @@
export const INSURANCE_CATEGORIES = [
'Health Insurance',
'Life Insurance',
'Motor Insurance',
'Travel Insurance',
'Property Insurance',
] as const;
export const INSURANCE_PAYMENT_METHODS = ['UPI', 'Net Banking', 'Card'] as const;
export const INSURANCE_PREMIUM_FREQUENCIES = ['Monthly', 'Quarterly', 'Yearly'] as const;
export const INSURANCE_POLICY_STATUSES = ['Active', 'Renewal Due', 'Expired', 'Grace Period'] as const;
export const INSURANCE_CLAIM_STATUSES = [
'Submitted',
'Under Review',
'Approved',
'Rejected',
'Settled',
] as const;
export const INSURANCE_DOCUMENT_STATUSES = ['uploaded', 'verified', 'rejected'] as const;
export type InsuranceCategory = (typeof INSURANCE_CATEGORIES)[number];
export type InsurancePaymentMethod = (typeof INSURANCE_PAYMENT_METHODS)[number];
export type InsurancePremiumFrequency = (typeof INSURANCE_PREMIUM_FREQUENCIES)[number];
export type InsurancePolicyStatus = (typeof INSURANCE_POLICY_STATUSES)[number];
export type InsuranceClaimStatus = (typeof INSURANCE_CLAIM_STATUSES)[number];
export type InsuranceDocumentStatus = (typeof INSURANCE_DOCUMENT_STATUSES)[number];
export type InsuranceNotificationType =
| 'premium_due'
| 'policy_expiry'
| 'claim_update'
| 'offer';
export type InsuranceDocumentCategory = 'kyc' | 'claim';
export type InsuranceDocumentType =
| 'Aadhaar'
| 'PAN'
| 'Passport'
| 'Driving License'
| 'RC Book'
| 'Medical Report'
| 'Travel Ticket'
| 'Property Proof'
| 'Invoice'
| 'Photo'
| 'Other';
export type InsuranceGender = 'Male' | 'Female' | 'Other';
export type InsurancePurchaseStep = 'details' | 'plans' | 'payment';
export interface InsuranceDynamicField {
key: string;
label: string;
placeholder: string;
keyboardType?: 'default' | 'numeric' | 'email-address';
required?: boolean;
multiline?: boolean;
}
export interface InsuranceCategoryMeta {
title: InsuranceCategory;
icon: string;
accentColor: string;
description: string;
specificFields: InsuranceDynamicField[];
}
export const INSURANCE_CATEGORY_META: Record<InsuranceCategory, InsuranceCategoryMeta> = {
'Health Insurance': {
title: 'Health Insurance',
icon: 'medkit-outline',
accentColor: '#10B981',
description: 'Cashless hospitalization, wellness support, and family protection.',
specificFields: [
{
key: 'familyMembers',
label: 'Family members covered',
placeholder: 'Enter number of members',
keyboardType: 'numeric',
required: true,
},
{
key: 'medicalHistory',
label: 'Medical history',
placeholder: 'Declare existing conditions or enter None',
required: true,
multiline: true,
},
{
key: 'smoker',
label: 'Smoker status',
placeholder: 'Yes or No',
required: true,
},
],
},
'Life Insurance': {
title: 'Life Insurance',
icon: 'heart-circle-outline',
accentColor: '#F59E0B',
description: 'Long-term income protection with nominee-first policy servicing.',
specificFields: [
{
key: 'annualIncome',
label: 'Annual income',
placeholder: 'Enter annual income',
keyboardType: 'numeric',
required: true,
},
{
key: 'occupation',
label: 'Occupation',
placeholder: 'Enter occupation',
required: true,
},
{
key: 'smoker',
label: 'Smoker status',
placeholder: 'Yes or No',
required: true,
},
],
},
'Motor Insurance': {
title: 'Motor Insurance',
icon: 'car-sport-outline',
accentColor: '#3B82F6',
description: 'Comprehensive protection for car and bike ownership journeys.',
specificFields: [
{
key: 'vehicleType',
label: 'Vehicle type',
placeholder: 'Car or Bike',
required: true,
},
{
key: 'makeModel',
label: 'Vehicle make and model',
placeholder: 'Enter make and model',
required: true,
},
{
key: 'registrationNumber',
label: 'Registration number',
placeholder: 'Enter registration number',
required: true,
},
{
key: 'registrationYear',
label: 'Registration year',
placeholder: 'Enter registration year',
keyboardType: 'numeric',
required: true,
},
{
key: 'previousClaims',
label: 'Previous claims',
placeholder: 'Enter count of claims in last 3 years',
keyboardType: 'numeric',
required: true,
},
],
},
'Travel Insurance': {
title: 'Travel Insurance',
icon: 'airplane-outline',
accentColor: '#8B5CF6',
description: 'Trip protection for medical emergencies, delays, and baggage loss.',
specificFields: [
{
key: 'destination',
label: 'Destination',
placeholder: 'Enter destination country or region',
required: true,
},
{
key: 'departureDate',
label: 'Departure date',
placeholder: 'YYYY-MM-DD',
required: true,
},
{
key: 'returnDate',
label: 'Return date',
placeholder: 'YYYY-MM-DD',
required: true,
},
{
key: 'travellers',
label: 'Number of travellers',
placeholder: 'Enter number of travellers',
keyboardType: 'numeric',
required: true,
},
{
key: 'passportNumber',
label: 'Passport number',
placeholder: 'Enter passport number',
required: true,
},
],
},
'Property Insurance': {
title: 'Property Insurance',
icon: 'home-outline',
accentColor: '#EF4444',
description: 'Cover for home structure, contents, and landlord liability.',
specificFields: [
{
key: 'propertyType',
label: 'Property type',
placeholder: 'Apartment, Villa, Office, etc.',
required: true,
},
{
key: 'propertyValue',
label: 'Property value',
placeholder: 'Enter insured property value',
keyboardType: 'numeric',
required: true,
},
{
key: 'constructionYear',
label: 'Construction year',
placeholder: 'Enter construction year',
keyboardType: 'numeric',
required: true,
},
{
key: 'propertyAddress',
label: 'Property address',
placeholder: 'Enter complete insured address',
required: true,
multiline: true,
},
],
},
};
export interface InsuranceInsurer {
id: string;
name: string;
claimSettlementRatio: number;
supportPhone: string;
supportEmail: string;
website: string;
address: string;
}
export interface InsurancePlan {
id: string;
category: InsuranceCategory;
insurerId: string;
insurerName: string;
name: string;
premium: number;
premiumFrequency: InsurancePremiumFrequency;
coverAmount: number;
tenureYears: number;
summary: string;
benefits: string[];
inclusions: string[];
exclusions: string[];
claimSettlementRatio: number;
waitingPeriodDays?: number;
deductibleAmount?: number;
recommendedFor?: string;
addOns?: string[];
}
export interface InsurancePolicyDocument {
id: string;
name: string;
mimeType: string;
uploadedAt: string;
url: string;
sizeKb: number;
}
export interface InsurancePolicy {
id: string;
planId: string;
planName: string;
category: InsuranceCategory;
insurer: InsuranceInsurer;
policyNumber: string;
insuredName: string;
premiumAmount: number;
premiumFrequency: InsurancePremiumFrequency;
coverageAmount: number;
status: InsurancePolicyStatus;
startDate: string;
expiryDate: string;
nextPremiumDueDate: string;
benefits: string[];
inclusions: string[];
exclusions: string[];
nomineeName: string;
nomineeRelationship: string;
autoRenewEnabled: boolean;
membersCovered: number;
deductibleAmount?: number;
vehicleDetails?: {
vehicleType: string;
makeModel: string;
registrationNumber: string;
registrationYear: string;
};
healthDetails?: {
familyMembers: number;
medicalHistory: string;
smoker: string;
};
lifeDetails?: {
annualIncome: number;
occupation: string;
smoker: string;
};
travelDetails?: {
destination: string;
departureDate: string;
returnDate: string;
travellers: number;
passportNumber: string;
};
propertyDetails?: {
propertyType: string;
propertyValue: number;
constructionYear: string;
propertyAddress: string;
};
documents: InsurancePolicyDocument[];
}
export interface InsurancePremiumDue {
id: string;
policyId: string;
planName: string;
policyNumber: string;
amount: number;
dueDate: string;
frequency: InsurancePremiumFrequency;
lateFee: number;
autoRenewEnabled: boolean;
}
export interface InsuranceDocument {
id: string;
name: string;
type: InsuranceDocumentType;
category: InsuranceDocumentCategory;
status: InsuranceDocumentStatus;
uploadedAt: string;
mimeType: string;
url: string;
sizeKb: number;
linkedClaimId?: string;
rejectionReason?: string;
}
export interface InsuranceClaim {
id: string;
claimNumber: string;
policyId: string;
policyNumber: string;
policyName: string;
category: InsuranceCategory;
reason: string;
eventDate: string;
amount: number;
status: InsuranceClaimStatus;
createdAt: string;
updatedAt: string;
notes: string;
documents: InsuranceDocument[];
}
export interface InsuranceNotification {
id: string;
type: InsuranceNotificationType;
title: string;
body: string;
createdAt: string;
read: boolean;
policyId?: string;
claimId?: string;
}
export interface InsuranceDashboardPayload {
policies: InsurancePolicy[];
totalCoverageAmount: number;
activePoliciesCount: number;
duePremiums: InsurancePremiumDue[];
upcomingPremium: InsurancePremiumDue | null;
recentClaims: InsuranceClaim[];
notifications: InsuranceNotification[];
}
export interface InsurancePersonalDetails {
fullName: string;
age: string;
gender: InsuranceGender | '';
email: string;
phone: string;
}
export interface InsuranceNomineeDetails {
name: string;
relationship: string;
age: string;
}
export interface InsuranceAddressDetails {
line1: string;
city: string;
state: string;
postalCode: string;
}
export interface InsuranceKycDetails {
aadhaarNumber: string;
panNumber: string;
}
export interface InsurancePurchaseDraft {
category: InsuranceCategory;
personal: InsurancePersonalDetails;
nominee: InsuranceNomineeDetails;
address: InsuranceAddressDetails;
kyc: InsuranceKycDetails;
specificDetails: Record<string, string>;
selectedPlanId: string | null;
paymentMethod: InsurancePaymentMethod;
autoRenewEnabled: boolean;
comparePlanIds: string[];
}
export interface InsuranceClaimDraft {
policyId: string;
reason: string;
eventDate: string;
amount: string;
description: string;
documentIds: string[];
}
export interface InsuranceCalculatorInput {
category: InsuranceCategory;
age: string;
coverageAmount: string;
tenureYears: string;
}
export interface InsuranceCalculatorEstimate {
monthlyPremium: number;
annualPremium: number;
recommendedCover: number;
}
export interface PurchaseInsurancePayload {
draft: InsurancePurchaseDraft;
otp: string;
}
export interface PurchaseInsuranceResult {
policy: InsurancePolicy;
transactionRef: string;
}
export interface PayInsurancePremiumPayload {
policyId: string;
amount: number;
method: InsurancePaymentMethod;
otp: string;
}
export interface PayInsurancePremiumResult {
success: boolean;
transactionRef: string;
nextDueDate: string;
}
export interface SubmitInsuranceClaimPayload {
policyId: string;
reason: string;
eventDate: string;
amount: number;
description: string;
documentIds: string[];
otp: string;
}
export interface SubmitInsuranceClaimResult {
claim: InsuranceClaim;
referenceId: string;
}
export interface UploadInsuranceDocumentPayload {
name: string;
type: InsuranceDocumentType;
category: InsuranceDocumentCategory;
mimeType: string;
uri: string;
sizeKb: number;
linkedClaimId?: string;
existingDocumentId?: string;
}
export interface RequestInsuranceOtpPayload {
action: 'purchase' | 'claim' | 'payment';
policyId?: string;
}
export interface InsuranceOtpSession {
referenceId: string;
destinationMask: string;
mockCodeHint?: string;
}
@@ -0,0 +1,135 @@
import type {
InsuranceCalculatorEstimate,
InsuranceCalculatorInput,
InsuranceCategory,
InsurancePolicy,
InsurancePremiumDue,
InsurancePremiumFrequency,
} from '../types';
const CATEGORY_RATE_MAP: Record<InsuranceCategory, number> = {
'Health Insurance': 0.024,
'Life Insurance': 0.006,
'Motor Insurance': 0.031,
'Travel Insurance': 0.012,
'Property Insurance': 0.01,
};
const CATEGORY_RECOMMENDED_MULTIPLIER: Record<InsuranceCategory, number> = {
'Health Insurance': 6,
'Life Insurance': 150,
'Motor Insurance': 1.2,
'Travel Insurance': 4,
'Property Insurance': 1,
};
const DAY_IN_MS = 24 * 60 * 60 * 1000;
export const addMonths = (value: string, months: number) => {
const date = new Date(value);
const next = new Date(date);
next.setMonth(date.getMonth() + months);
return next.toISOString();
};
export const addYears = (value: string, years: number) => {
const date = new Date(value);
const next = new Date(date);
next.setFullYear(date.getFullYear() + years);
return next.toISOString();
};
export const getNextDateForFrequency = (
value: string,
frequency: InsurancePremiumFrequency,
) => {
if (frequency === 'Monthly') {
return addMonths(value, 1);
}
if (frequency === 'Quarterly') {
return addMonths(value, 3);
}
return addYears(value, 1);
};
export const getRecommendedCover = (
category: InsuranceCategory,
baseAmount: number,
) => Math.round(baseAmount * CATEGORY_RECOMMENDED_MULTIPLIER[category]);
export const estimateInsurancePremium = (
input: InsuranceCalculatorInput,
): InsuranceCalculatorEstimate => {
const age = Number(input.age || 0);
const coverageAmount = Number(input.coverageAmount || 0);
const tenureYears = Number(input.tenureYears || 0);
const normalizedAge = age > 0 ? age : 28;
const normalizedCoverage = coverageAmount > 0 ? coverageAmount : 500000;
const normalizedTenure = tenureYears > 0 ? tenureYears : 1;
const baseRate = CATEGORY_RATE_MAP[input.category];
const ageFactor =
normalizedAge < 30 ? 0.92 : normalizedAge < 40 ? 1 : normalizedAge < 50 ? 1.18 : 1.36;
const coverageFactor =
normalizedCoverage >= 10000000 ? 0.95 : normalizedCoverage >= 5000000 ? 0.98 : 1;
const tenureFactor = normalizedTenure >= 5 ? 0.92 : normalizedTenure >= 3 ? 0.96 : 1;
const annualPremium = Math.round(
normalizedCoverage * baseRate * ageFactor * coverageFactor * tenureFactor,
);
return {
monthlyPremium: Math.max(Math.round(annualPremium / 12), 1),
annualPremium,
recommendedCover: getRecommendedCover(input.category, normalizedCoverage),
};
};
export const estimateAnnualPremium = (monthlyPremium: number) => monthlyPremium * 12;
export const getPolicyTotalCoverage = (policies: InsurancePolicy[]) =>
policies.reduce((sum, policy) => sum + policy.coverageAmount, 0);
export const getEarliestPremiumDue = (dues: InsurancePremiumDue[]) =>
[...dues].sort(
(left, right) => new Date(left.dueDate).getTime() - new Date(right.dueDate).getTime(),
)[0] ?? null;
export const getDaysUntil = (value: string) =>
Math.max(Math.ceil((new Date(value).getTime() - Date.now()) / DAY_IN_MS), 0);
export const getPremiumLateFee = (dueDate: string, amount: number) => {
const daysLate = Math.floor((Date.now() - new Date(dueDate).getTime()) / DAY_IN_MS);
if (daysLate <= 0) {
return 0;
}
return Math.min(Math.round(amount * 0.015), 2000);
};
export const getPolicyStatusFromDueDate = (
nextPremiumDueDate: string,
expiryDate: string,
) => {
const dueAt = new Date(nextPremiumDueDate).getTime();
const expiryAt = new Date(expiryDate).getTime();
const now = Date.now();
if (expiryAt < now) {
return 'Expired' as const;
}
if (dueAt < now) {
return 'Grace Period' as const;
}
if (dueAt - now <= 15 * DAY_IN_MS) {
return 'Renewal Due' as const;
}
return 'Active' as const;
};
@@ -0,0 +1,160 @@
import { isValidOtp, isValidPhone } from '../../../utils/validation';
import type {
InsuranceCategory,
InsuranceClaimDraft,
InsuranceDocumentType,
InsurancePurchaseDraft,
} from '../types';
import { INSURANCE_CATEGORY_META } from '../types';
const isValidEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
const isValidPan = (value: string) =>
/^[A-Z]{5}[0-9]{4}[A-Z]$/.test(value.trim().toUpperCase());
const isValidAadhaar = (value: string) => /^[0-9]{12}$/.test(value.replace(/\s/g, ''));
const isValidDateInput = (value: string) =>
/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(value) && !Number.isNaN(new Date(value).getTime());
export type InsuranceValidationErrors = Record<string, string>;
export const validateInsurancePurchaseDraft = (
draft: InsurancePurchaseDraft,
category: InsuranceCategory,
): InsuranceValidationErrors => {
const errors: InsuranceValidationErrors = {};
const categoryMeta = INSURANCE_CATEGORY_META[category];
if (!draft.personal.fullName.trim()) {
errors.personalFullName = 'Full name is required.';
}
if (!draft.personal.age.trim() || Number(draft.personal.age) < 18) {
errors.personalAge = 'Applicant must be at least 18 years old.';
}
if (!draft.personal.gender) {
errors.personalGender = 'Gender is required.';
}
if (!isValidEmail(draft.personal.email)) {
errors.personalEmail = 'Enter a valid email address.';
}
if (!isValidPhone(draft.personal.phone)) {
errors.personalPhone = 'Enter a valid 10-digit mobile number.';
}
if (!draft.nominee.name.trim()) {
errors.nomineeName = 'Nominee name is required.';
}
if (!draft.nominee.relationship.trim()) {
errors.nomineeRelationship = 'Nominee relationship is required.';
}
if (!draft.nominee.age.trim()) {
errors.nomineeAge = 'Nominee age is required.';
}
if (!draft.address.line1.trim()) {
errors.addressLine1 = 'Address is required.';
}
if (!draft.address.city.trim()) {
errors.addressCity = 'City is required.';
}
if (!draft.address.state.trim()) {
errors.addressState = 'State is required.';
}
if (!/^[0-9]{6}$/.test(draft.address.postalCode.trim())) {
errors.addressPostalCode = 'Enter a valid 6-digit PIN code.';
}
if (!isValidAadhaar(draft.kyc.aadhaarNumber)) {
errors.kycAadhaar = 'Enter a valid 12-digit Aadhaar number.';
}
if (!isValidPan(draft.kyc.panNumber)) {
errors.kycPan = 'Enter a valid PAN number.';
}
categoryMeta.specificFields.forEach((field) => {
const value = draft.specificDetails[field.key]?.trim() ?? '';
if (field.required && !value) {
errors[`specific_${field.key}`] = `${field.label} is required.`;
return;
}
if (!value) {
return;
}
if (field.keyboardType === 'numeric' && Number(value) <= 0) {
errors[`specific_${field.key}`] = `Enter a valid ${field.label.toLowerCase()}.`;
return;
}
if (field.key.toLowerCase().includes('date') && !isValidDateInput(value)) {
errors[`specific_${field.key}`] = `Use YYYY-MM-DD for ${field.label.toLowerCase()}.`;
}
});
return errors;
};
export const validateInsuranceClaimDraft = (
draft: InsuranceClaimDraft,
): InsuranceValidationErrors => {
const errors: InsuranceValidationErrors = {};
if (!draft.policyId) {
errors.policyId = 'Choose a policy to continue.';
}
if (!draft.reason.trim()) {
errors.reason = 'Claim reason is required.';
}
if (!isValidDateInput(draft.eventDate)) {
errors.eventDate = 'Use YYYY-MM-DD for event date.';
}
if (!draft.amount.trim() || Number(draft.amount) <= 0) {
errors.amount = 'Enter a valid claim amount.';
}
if (!draft.description.trim()) {
errors.description = 'Brief claim details are required.';
}
if (!draft.documentIds.length) {
errors.documents = 'Upload at least one supporting document.';
}
return errors;
};
export const validateInsuranceOtp = (otp: string) =>
isValidOtp(otp) ? undefined : 'Enter the 6-digit OTP.';
export const validateInsuranceDocumentUpload = (
name: string,
sizeKb: number,
type: InsuranceDocumentType,
) => {
if (!name.trim()) {
return 'Document name is required.';
}
if (sizeKb <= 0 || sizeKb > 10240) {
return 'File size should be between 1 KB and 10 MB.';
}
if (!type) {
return 'Document type is required.';
}
return undefined;
};