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
+14
View File
@@ -0,0 +1,14 @@
import axios from 'axios';
import { env } from '../../config/env';
import { attachInterceptors } from './interceptors';
export const apiClient = axios.create({
baseURL: env.apiBaseUrl,
timeout: 15000,
headers: {
'Content-Type': 'application/json',
},
});
attachInterceptors(apiClient);
+50
View File
@@ -0,0 +1,50 @@
import {
BaseQueryFn,
createApi,
} from '@reduxjs/toolkit/query/react';
import type { AxiosRequestConfig, AxiosError } from 'axios';
import { QUERY_TAGS } from '../../constants/queryTags';
import type { ApiError } from '../../types/api';
import { apiClient } from './apiClient';
export interface AxiosBaseQueryArgs {
url: string;
method?: AxiosRequestConfig['method'];
data?: unknown;
params?: Record<string, unknown>;
headers?: Record<string, string>;
}
const axiosBaseQuery =
(): BaseQueryFn<AxiosBaseQueryArgs, unknown, ApiError> =>
async ({ url, method = 'GET', data, params, headers }) => {
try {
const response = await apiClient({
url,
method,
data,
...(params ? { params } : {}),
...(headers ? { headers } : {}),
});
return { data: response.data };
} catch (error) {
const axiosError = error as AxiosError;
return {
error: {
status: Number(axiosError.response?.status ?? 500),
message: axiosError.message || 'Something went wrong',
data: axiosError.response?.data,
},
};
}
};
export const baseApi = createApi({
reducerPath: 'api',
baseQuery: axiosBaseQuery(),
tagTypes: [...QUERY_TAGS],
endpoints: () => ({}),
});
+102
View File
@@ -0,0 +1,102 @@
export const endpoints = {
auth: {
login: '/auth/login',
verifyOtp: '/auth/verify-otp',
setupMpin: '/auth/setup-mpin',
},
dashboard: {
summary: '/dashboard/summary',
},
transfer: {
beneficiaries: '/transfers/beneficiaries',
submit: '/transfers',
},
payments: {
upi: '/payments/upi',
},
deposits: {
products: '/deposits/products',
fixed: {
list: '/fd/list',
create: '/fd/create',
detail: (id: string) => `/fd/${id}`,
close: '/fd/close',
},
recurring: {
list: '/rd/list',
create: '/rd/create',
detail: (id: string) => `/rd/${id}`,
close: '/rd/close',
},
},
services: {
requests: '/services/requests',
chat: '/services/chat',
},
loans: {
products: '/loans/products',
active: '/loans/active',
detail: (id: string) => `/loans/${id}`,
apply: '/loans/apply',
eligibility: '/loans/eligibility',
emiSchedule: (id: string) => `/loans/${id}/emi-schedule`,
payEmi: '/loans/pay-emi',
foreclosureQuote: (id: string) => `/loans/${id}/foreclosure`,
prepay: (id: string) => `/loans/${id}/prepay`,
applications: '/loans/applications',
documents: '/loans/documents',
uploadDocument: '/loans/documents/upload',
history: '/loans/history',
notifications: '/loans/notifications',
},
insurance: {
dashboard: '/insurance/dashboard',
plans: '/insurance/plans',
comparePlans: '/insurance/plans/compare',
calculator: '/insurance/calculator',
policies: '/insurance/policies',
policyDetail: (id: string) => `/insurance/policies/${id}`,
purchase: '/insurance/policies/purchase',
premiumDues: '/insurance/premiums/due',
payPremium: '/insurance/premiums/pay',
autoRenew: (policyId: string) => `/insurance/policies/${policyId}/auto-renew`,
claims: '/insurance/claims',
submitClaim: '/insurance/claims/submit',
documents: '/insurance/documents',
uploadDocument: '/insurance/documents/upload',
notifications: '/insurance/notifications',
markNotificationRead: (notificationId: string) =>
`/insurance/notifications/${notificationId}/read`,
otp: '/insurance/otp',
},
cards: {
overview: '/cards',
detail: '/cards/detail',
controls: '/cards/controls',
limits: '/cards/limits',
status: '/cards/status',
notifications: '/cards/notifications',
virtualCard: '/cards/virtual-card',
statements: '/cards/statements',
rewards: '/cards/rewards',
settings: '/cards/settings',
},
profile: {
me: '/profile/me',
},
branches: {
list: '/branches',
detail: (id: string) => `/branches/${id}`,
},
agent: {
dashboard: '/agent/dashboard',
customers: '/agent/customers',
customerDetail: (id: string) => `/agent/customers/${id}`,
collections: '/agent/collections',
collectionHistory: '/agent/collections/history',
commission: '/agent/commission',
loans: '/agent/loans',
sms: '/agent/sms',
routes: '/agent/routes',
},
};
+39
View File
@@ -0,0 +1,39 @@
import type { AxiosError, AxiosInstance } from 'axios';
import { secureStorage } from '../storage/secureStorage';
const unauthorizedHandlers = new Set<() => void>();
export const registerUnauthorizedHandler = (handler: () => void) => {
unauthorizedHandlers.add(handler);
return () => {
unauthorizedHandlers.delete(handler);
};
};
export const attachInterceptors = (client: AxiosInstance) => {
client.interceptors.request.use(async (config) => {
const token = await secureStorage.getAccessToken();
if (token) {
const nextHeaders = (config.headers ?? {}) as Record<string, string>;
nextHeaders.Authorization = `Bearer ${token}`;
config.headers = nextHeaders as never;
}
return config;
});
client.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
if (error.response?.status === 401) {
await secureStorage.clearSession();
unauthorizedHandlers.forEach((handler) => handler());
}
return Promise.reject(error);
},
);
};
+52
View File
@@ -0,0 +1,52 @@
import type { AxiosError } from 'axios';
import { appConfig } from '../../config/appConfig';
import type { ApiError } from '../../types/api';
import { sleep } from '../../utils/async';
const isApiError = (value: unknown): value is ApiError =>
typeof value === 'object' &&
value !== null &&
'status' in value &&
'message' in value;
export const toApiError = (error: unknown): ApiError => {
if (isApiError(error)) {
return error;
}
const axiosError = error as AxiosError;
return {
status: Number(axiosError.response?.status ?? 500),
message: axiosError.message || 'Unexpected service error',
data: axiosError.response?.data,
};
};
export const resolveWithMock = async <T>({
mockData,
request,
delayMs = 320,
}: {
mockData: T | (() => T);
request: () => Promise<T>;
delayMs?: number;
}): Promise<{ data: T } | { error: ApiError }> => {
if (appConfig.features.useMockServer) {
await sleep(delayMs);
return {
data: typeof mockData === 'function' ? (mockData as () => T)() : mockData,
};
}
try {
return {
data: await request(),
};
} catch (error) {
return {
error: toApiError(error),
};
}
};
+18
View File
@@ -0,0 +1,18 @@
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
const options = {
enableVibrateFallback: true,
ignoreAndroidSystemSettings: false,
};
export const haptics = {
success() {
ReactNativeHapticFeedback.trigger('notificationSuccess', options);
},
error() {
ReactNativeHapticFeedback.trigger('notificationError', options);
},
selection() {
ReactNativeHapticFeedback.trigger('selection', options);
},
};
@@ -0,0 +1,9 @@
import { PermissionsAndroid, Platform } from 'react-native';
export const registerForPushNotifications = async () => {
if (Platform.OS === 'android' && Platform.Version >= 33) {
return PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS);
}
return 'granted';
};
+23
View File
@@ -0,0 +1,23 @@
import ReactNativeBiometrics from 'react-native-biometrics';
import { appConfig } from '../../config/appConfig';
const biometricsClient = new ReactNativeBiometrics();
export const biometrics = {
async isAvailable() {
const { available } = await biometricsClient.isSensorAvailable();
return available;
},
async prompt() {
try {
return await biometricsClient.simplePrompt({
promptMessage: appConfig.security.biometricPromptTitle,
});
} catch {
return { success: false };
}
},
};
+17
View File
@@ -0,0 +1,17 @@
import { appConfig } from '../../config/appConfig';
let lastActivityAt = Date.now();
export const sessionManager = {
touch() {
lastActivityAt = Date.now();
},
getIdleDurationMs() {
return Date.now() - lastActivityAt;
},
hasExpired() {
return this.getIdleDurationMs() >= appConfig.security.sessionTimeoutMs;
},
};
+49
View File
@@ -0,0 +1,49 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Keychain from 'react-native-keychain';
import type { AuthTokens } from '../../types/app';
const BIOMETRIC_KEY = 'bankcbs.biometricEnabled';
const SESSION_SERVICE = 'com.bankcbs.mobile.session';
export const secureStorage = {
async saveSession(tokens: AuthTokens) {
await Keychain.setGenericPassword('session', JSON.stringify(tokens), {
service: SESSION_SERVICE,
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
securityLevel: Keychain.SECURITY_LEVEL.SECURE_SOFTWARE,
});
},
async getSession(): Promise<AuthTokens | null> {
const credentials = await Keychain.getGenericPassword({
service: SESSION_SERVICE,
});
if (!credentials) {
return null;
}
return JSON.parse(credentials.password) as AuthTokens;
},
async getAccessToken() {
const session = await this.getSession();
return session?.accessToken ?? null;
},
async clearSession() {
await Keychain.resetGenericPassword({
service: SESSION_SERVICE,
});
},
async setBiometricEnabled(value: boolean) {
await AsyncStorage.setItem(BIOMETRIC_KEY, JSON.stringify(value));
},
async getBiometricEnabled() {
const value = await AsyncStorage.getItem(BIOMETRIC_KEY);
return value ? JSON.parse(value) : false;
},
};