Fix form keyboard issues and sticky footers in agent screens
This commit is contained in:
@@ -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);
|
||||
@@ -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: () => ({}),
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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';
|
||||
};
|
||||
@@ -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 };
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user