Update CBS Mobile App codebase

This commit is contained in:
Abin Toppo
2026-09-21 13:21:25 +05:30
parent 58af541536
commit 2dcb10775c
339 changed files with 26750 additions and 16767 deletions
+141 -86
View File
@@ -1,96 +1,151 @@
import type { AuthTokens, UserProfile } from '../../../types/app';
import { demoUser } from '../../../constants/mockData';
import { endpoints } from '../../../services/api/endpoints';
import { env } from '../../../config/env';
import { apiClient } from '../../../services/api/apiClient';
import { baseApi } from '../../../services/api/baseApi';
import { resolveWithMock } from '../../../services/api/serviceHelpers';
import { isCbsApiError } from '../../../services/api/cbsErrors';
import { endpoints } from '../../../services/api/endpoints';
import { deviceIdentity } from '../../../services/security/deviceIdentity';
import { mpinCrypto } from '../../../services/security/mpinCrypto';
import type {
CreateMpinResult,
LoginResult,
SessionIdentity,
StartRegistrationResult,
VerifyOtpResult,
} from '../../../types/cbs';
interface LoginRequest {
phone: string;
password: string;
}
/**
* `MALFORMED_REQUEST` on an MPIN call is what a stale or wrong public key looks
* like from the client — the server will not say more, because saying more
* would leak whether the profile exists. So drop the cached key, re-fetch, and
* try exactly once more. A second failure is a real problem worth surfacing.
*/
const withKeyRetry = async <T>(call: () => Promise<T>): Promise<T> => {
try {
return await call();
} catch (error) {
if (isCbsApiError(error) && error.code === 'MALFORMED_REQUEST') {
mpinCrypto.invalidate();
return call();
}
interface LoginResponse {
challengeId: string;
phone: string;
}
throw error;
}
};
interface VerifyOtpRequest {
challengeId: string;
otp: string;
phone: string;
}
export const authService = {
/**
* Looks the customer up in their bank's core system, links every account it
* returns and sends an OTP to the handset.
*/
async startRegistration(mobileNumber: string, orgId: string = env.orgId) {
const response = await apiClient.post<StartRegistrationResult>(
endpoints.registration.start,
{ orgId, mobileNumber },
);
interface VerifyOtpResponse {
verified: true;
phone: string;
}
return response.data;
},
interface SetupMpinRequest {
phone: string;
mpin: string;
}
/**
* Verifies the OTP, binds the profile to this device and allocates an MMID to
* every linked account. Returns the single-use, five-minute setup token that
* MPIN creation requires.
*/
async verifyRegistrationOtp({
mobileNumber,
otp,
orgId = env.orgId,
}: {
mobileNumber: string;
otp: string;
orgId?: string;
}) {
const deviceId = await deviceIdentity.get();
interface SetupMpinResponse {
tokens: AuthTokens;
user: UserProfile;
}
const response = await apiClient.post<VerifyOtpResult>(endpoints.registration.verifyOtp, {
orgId,
mobileNumber,
otp,
deviceId,
});
const createDemoTokens = (): AuthTokens => ({
accessToken: 'demo-access-token',
refreshToken: 'demo-refresh-token',
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(),
});
return response.data;
},
export const authApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
login: builder.mutation<LoginResponse, LoginRequest>({
queryFn: ({ phone }) =>
resolveWithMock({
mockData: {
challengeId: 'otp_challenge_001',
phone,
},
request: async () => {
const response = await apiClient.post<LoginResponse>(endpoints.auth.login, { phone });
return response.data;
},
}),
}),
verifyOtp: builder.mutation<VerifyOtpResponse, VerifyOtpRequest>({
queryFn: ({ phone }) =>
resolveWithMock({
mockData: {
verified: true,
phone,
},
request: async () => {
const response = await apiClient.post<VerifyOtpResponse>(endpoints.auth.verifyOtp, {
phone,
});
return response.data;
},
}),
}),
setupMpin: builder.mutation<SetupMpinResponse, SetupMpinRequest>({
queryFn: () =>
resolveWithMock({
mockData: () => ({
tokens: createDemoTokens(),
user: demoUser,
}),
request: async () => {
const response = await apiClient.post<SetupMpinResponse>(endpoints.auth.setupMpin);
return response.data;
},
}),
invalidatesTags: ['Auth', 'User'],
}),
}),
});
/**
* Creates the MPIN and completes registration. There is deliberately no
* orgId and no mobile number in the request — both come from the setup token,
* which is what stops a caller setting someone else's MPIN. The deviceId must
* match the one sent at verify-otp.
*/
async createMpin({ setupToken, mpin }: { setupToken: string; mpin: string }) {
return withKeyRetry(async () => {
const deviceId = await deviceIdentity.get();
const encryptedMpin = await mpinCrypto.encryptMpin(mpin);
export const {
useLoginMutation,
useVerifyOtpMutation,
useSetupMpinMutation,
} = authApi;
const response = await apiClient.post<CreateMpinResult>(endpoints.registration.mpin, {
setupToken,
deviceId,
encryptedMpin,
});
return response.data;
});
},
/**
* Verifies the MPIN and opens a session. The MPIN is encrypted exactly as it
* was at setup — same key, same payload shape.
*/
async login({
mobileNumber,
mpin,
orgId = env.orgId,
setupToken,
}: {
mobileNumber: string;
mpin: string;
orgId?: string;
/** See `LoginRequest.setupToken` — a device re-bind, and only that. */
setupToken?: string | null;
}) {
return withKeyRetry(async () => {
const deviceId = await deviceIdentity.get();
const encryptedMpin = await mpinCrypto.encryptMpin(mpin);
const response = await apiClient.post<LoginResult>(endpoints.auth.login, {
orgId,
mobileNumber,
deviceId,
encryptedMpin,
// Spread rather than set: an ordinary sign-in must not carry the key at
// all, and `setupToken: undefined` is a key once this goes through
// JSON.stringify on some clients.
...(setupToken ? { setupToken } : {}),
});
return response.data;
});
},
/** Who the current session belongs to. Doubles as a keep-alive. */
async getIdentity() {
const response = await apiClient.get<SessionIdentity>(endpoints.auth.me);
return response.data;
},
/**
* Revokes the session immediately. The endpoint always answers 200, even for
* a token that was already dead — a different answer would tell a caller
* whether a token they hold is live. Nothing that happens here may block the
* local sign-out, so every failure is swallowed: the customer asked to be
* signed out, and a network error must not leave them signed in.
*/
async logout() {
try {
await apiClient.post(endpoints.auth.logout);
} catch {
// Intentionally ignored. The local session is cleared regardless.
}
},
};