Add global search, in-app + push notifications, and fix card/beneficiary/scroll issues
- Global feature search for the user app, wired to the home search icon - Notifications screen: swipe-to-delete, vanish animation, unread badge, and Notifee-backed foreground + background (scheduled) OS notifications - Fix Add Beneficiary save (single source of truth via beneficiaries slice) - Fix Cards tab scroll freeze and bottom blank space (use ScreenShell scroll) - User-friendly toast copy across the app - Android build tuning: parallel/caching/JVM G1 args, mobile-only ABIs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d7907e1d64
commit
58af541536
@@ -4,7 +4,6 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
|
||||
import { ScreenShell, Input, Button, Card, AppModal } from '../../../components';
|
||||
import { useAddBeneficiaryMutation } from '../services/paymentsApi';
|
||||
import { useTheme } from '../../../theme';
|
||||
import type { PaymentsStackParamList } from '../../../navigation/types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { beneficiariesActions } from '../store/beneficiariesSlice';
|
||||
@@ -12,7 +11,6 @@ import { beneficiariesActions } from '../store/beneficiariesSlice';
|
||||
type Props = NativeStackScreenProps<PaymentsStackParamList, 'AddBeneficiary'>;
|
||||
|
||||
export default function AddBeneficiaryScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const [addBen, { isLoading }] = useAddBeneficiaryMutation();
|
||||
|
||||
@@ -26,36 +24,53 @@ export default function AddBeneficiaryScreen({ navigation }: Props) {
|
||||
|
||||
const [otpModalVisible, setOtpModalVisible] = useState(false);
|
||||
const [otp, setOtp] = useState('');
|
||||
const [otpError, setOtpError] = useState<string | undefined>(undefined);
|
||||
|
||||
const isValid =
|
||||
form.name.length > 2 &&
|
||||
const isValid =
|
||||
form.name.trim().length > 2 &&
|
||||
form.accountNumber.length > 6 &&
|
||||
form.accountNumber === form.confirmAccountNumber &&
|
||||
form.ifsc.length === 11;
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!isValid) return;
|
||||
setOtp('');
|
||||
setOtpError(undefined);
|
||||
setOtpModalVisible(true);
|
||||
};
|
||||
|
||||
const confirmOTPAndSave = async () => {
|
||||
if (otp !== '123456') return; // Mock OTP validation
|
||||
|
||||
const closeOtpModal = () => {
|
||||
setOtpModalVisible(false);
|
||||
|
||||
setOtp('');
|
||||
setOtpError(undefined);
|
||||
};
|
||||
|
||||
const confirmOTPAndSave = async () => {
|
||||
if (otp !== '123456') {
|
||||
setOtpError('That code is incorrect. Use 123456 for this demo.');
|
||||
return;
|
||||
}
|
||||
|
||||
setOtpModalVisible(false);
|
||||
|
||||
try {
|
||||
const nickname = form.nickname.trim() || undefined;
|
||||
const newBeneficiaryObj = {
|
||||
name: form.name,
|
||||
bankName: form.ifsc.startsWith('HDFC') ? 'HDFC Bank' : 'State Bank of India', // mock logic
|
||||
name: form.name.trim(),
|
||||
bankName: form.ifsc.toUpperCase().startsWith('HDFC') ? 'HDFC Bank' : 'State Bank of India', // mock logic
|
||||
accountMasked: form.accountNumber.slice(-4).padStart(8, 'X'),
|
||||
nickname: form.nickname,
|
||||
nickname,
|
||||
};
|
||||
|
||||
|
||||
const payload = await addBen(newBeneficiaryObj).unwrap();
|
||||
dispatch(beneficiariesActions.addBeneficiary(payload));
|
||||
// Save into the beneficiaries slice — the single source of truth that
|
||||
// every payments screen reads, so the new payee shows up immediately.
|
||||
dispatch(beneficiariesActions.addBeneficiary({ ...payload, nickname }));
|
||||
navigation.goBack();
|
||||
} catch (e) {
|
||||
console.warn("Failed to add beneficiary", e);
|
||||
setOtpError('We could not save this payee. Please try again.');
|
||||
setOtpModalVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -84,12 +99,14 @@ export default function AddBeneficiaryScreen({ navigation }: Props) {
|
||||
onChangeText={(v) => setForm({ ...form, confirmAccountNumber: v })}
|
||||
error={form.confirmAccountNumber.length > 0 && form.accountNumber !== form.confirmAccountNumber ? "Account numbers must match" : undefined}
|
||||
/>
|
||||
<Input
|
||||
<Input
|
||||
label="IFSC Code"
|
||||
placeholder="e.g. SBIN0001234"
|
||||
autoCapitalize="characters"
|
||||
maxLength={11}
|
||||
value={form.ifsc}
|
||||
onChangeText={(v) => setForm({ ...form, ifsc: v })}
|
||||
error={form.ifsc.length > 0 && form.ifsc.length !== 11 ? 'IFSC code must be 11 characters' : undefined}
|
||||
/>
|
||||
<Input
|
||||
label="Nickname (Optional)"
|
||||
@@ -109,19 +126,23 @@ export default function AddBeneficiaryScreen({ navigation }: Props) {
|
||||
|
||||
<AppModal
|
||||
visible={otpModalVisible}
|
||||
onClose={() => setOtpModalVisible(false)}
|
||||
onClose={closeOtpModal}
|
||||
title="Bank Authentication"
|
||||
>
|
||||
<View style={{ gap: 16 }}>
|
||||
<Input
|
||||
<Input
|
||||
label="Enter 6-digit OTP"
|
||||
placeholder="OTP sent to mobile (Hint: 123456)"
|
||||
keyboardType="number-pad"
|
||||
maxLength={6}
|
||||
value={otp}
|
||||
onChangeText={setOtp}
|
||||
onChangeText={(v) => {
|
||||
setOtp(v);
|
||||
if (otpError) setOtpError(undefined);
|
||||
}}
|
||||
error={otpError}
|
||||
/>
|
||||
<Button
|
||||
<Button
|
||||
title="Confirm Addition"
|
||||
onPress={confirmOTPAndSave}
|
||||
disabled={otp.length !== 6 || isLoading}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
|
||||
import { ScreenShell, Card, Input } from '../../../components';
|
||||
import { useGetBeneficiariesQuery } from '../services/paymentsApi';
|
||||
import { useAppSelector } from '../../../app/hooks';
|
||||
import { selectBeneficiaries } from '../store/beneficiariesSlice';
|
||||
import { useTheme } from '../../../theme';
|
||||
import { TAB_BAR_HEIGHT } from '../../../utils/layoutConstants';
|
||||
import type { PaymentsStackParamList } from '../../../navigation/types';
|
||||
@@ -13,7 +14,7 @@ type Props = NativeStackScreenProps<PaymentsStackParamList, 'BeneficiariesList'>
|
||||
|
||||
export default function BeneficiariesListScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
const { data: beneficiaries = [] } = useGetBeneficiariesQuery();
|
||||
const beneficiaries = useAppSelector(selectBeneficiaries);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const filtered = beneficiaries.filter(b =>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, Text, View, ScrollView } from 'react-native';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { ScreenShell, Button, Card, AppModal, Input } from '../../../components';
|
||||
import { useGetBeneficiariesQuery, useSubmitBankTransferMutation } from '../services/paymentsApi';
|
||||
import { useSubmitBankTransferMutation } from '../services/paymentsApi';
|
||||
import { selectBeneficiaryById } from '../store/beneficiariesSlice';
|
||||
import { useTheme } from '../../../theme';
|
||||
import type { PaymentsStackParamList } from '../../../navigation/types';
|
||||
import { accountActions } from '../../home/store/accountSlice';
|
||||
@@ -16,8 +17,7 @@ export function PaymentConfirmationScreen({ navigation, route }: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const { beneficiaryId, transferType, amount, remarks } = route.params;
|
||||
|
||||
const { data: beneficiaries } = useGetBeneficiariesQuery();
|
||||
const beneficiary = beneficiaries?.find(b => b.id === beneficiaryId);
|
||||
const beneficiary = useSelector(selectBeneficiaryById(beneficiaryId));
|
||||
|
||||
const [submitTransfer, { isLoading }] = useSubmitBankTransferMutation();
|
||||
const [otpModalVisible, setOtpModalVisible] = useState(false);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
|
||||
import { ScreenShell, Input, Button, Card } from '../../../components';
|
||||
import { useGetBeneficiariesQuery } from '../services/paymentsApi';
|
||||
import { selectBeneficiaryById } from '../store/beneficiariesSlice';
|
||||
import { useTheme } from '../../../theme';
|
||||
import type { PaymentsStackParamList } from '../../../navigation/types';
|
||||
import type { RootState } from '../../../app/store';
|
||||
@@ -18,8 +18,7 @@ export function PaymentFormScreen({ navigation, route }: Props) {
|
||||
const theme = useTheme();
|
||||
const beneficiaryId = route.params?.beneficiaryId;
|
||||
const transferType = route.params?.transferType;
|
||||
const { data: beneficiaries } = useGetBeneficiariesQuery();
|
||||
const beneficiary = beneficiaries?.find(b => b.id === beneficiaryId);
|
||||
const beneficiary = useSelector(selectBeneficiaryById(beneficiaryId));
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [amount, setAmount] = useState('');
|
||||
|
||||
@@ -4,7 +4,7 @@ import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
|
||||
import { Card, ScreenShell } from '../../../components';
|
||||
import { useGetBeneficiariesQuery } from '../services/paymentsApi';
|
||||
import { selectBeneficiaries } from '../store/beneficiariesSlice';
|
||||
import { useTheme } from '../../../theme';
|
||||
import { useAppSelector } from '../../../app/hooks';
|
||||
import type { PaymentsStackParamList } from '../../../navigation/types';
|
||||
@@ -16,7 +16,7 @@ type Props = NativeStackScreenProps<PaymentsStackParamList, 'PaymentsHub'>;
|
||||
export default function PaymentsHomeScreen({ navigation }: Props) {
|
||||
const theme = useTheme();
|
||||
const transactions = useAppSelector(state => state.account.transactions);
|
||||
const { data: beneficiaries = [] } = useGetBeneficiariesQuery();
|
||||
const beneficiaries = useAppSelector(selectBeneficiaries);
|
||||
|
||||
// Simulated stats
|
||||
const dailyLimit = 500000;
|
||||
|
||||
@@ -4,7 +4,8 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
|
||||
import { ScreenShell, Card } from '../../../components';
|
||||
import { useGetBeneficiariesQuery } from '../services/paymentsApi';
|
||||
import { useAppSelector } from '../../../app/hooks';
|
||||
import { selectBeneficiaryById } from '../store/beneficiariesSlice';
|
||||
import { useTheme } from '../../../theme';
|
||||
import type { PaymentsStackParamList } from '../../../navigation/types';
|
||||
|
||||
@@ -13,8 +14,7 @@ type Props = NativeStackScreenProps<PaymentsStackParamList, 'TransferTypeSelecti
|
||||
export default function TransferTypeScreen({ navigation, route }: Props) {
|
||||
const theme = useTheme();
|
||||
const { beneficiaryId } = route.params;
|
||||
const { data: beneficiaries } = useGetBeneficiariesQuery();
|
||||
const beneficiary = beneficiaries?.find(b => b.id === beneficiaryId);
|
||||
const beneficiary = useAppSelector(selectBeneficiaryById(beneficiaryId));
|
||||
|
||||
const handleSelect = (type: 'IMPS' | 'NEFT' | 'RTGS') => {
|
||||
navigation.navigate('PaymentForm', { beneficiaryId, transferType: type });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { Beneficiary } from '../../../types/app';
|
||||
import type { RootState } from '../../../app/store';
|
||||
import { demoBeneficiaries } from '../../../constants/mockData';
|
||||
|
||||
interface BeneficiariesState {
|
||||
@@ -34,3 +35,9 @@ const beneficiariesSlice = createSlice({
|
||||
|
||||
export const beneficiariesActions = beneficiariesSlice.actions;
|
||||
export const beneficiariesReducer = beneficiariesSlice.reducer;
|
||||
|
||||
// Selectors — the slice is the single source of truth for saved payees, so a
|
||||
// newly added beneficiary is immediately visible everywhere it is read.
|
||||
export const selectBeneficiaries = (state: RootState) => state.beneficiaries.list;
|
||||
export const selectBeneficiaryById = (id: string) => (state: RootState) =>
|
||||
state.beneficiaries.list.find((beneficiary) => beneficiary.id === id);
|
||||
|
||||
Reference in New Issue
Block a user