Add TPIN creation, TPIN-authorised transfers, MPIN reset and transfer history
- Create TPIN: current MPIN -> OTP -> encrypted TPIN via /tpin/create/*, from Services and from the transfer screen on TPIN_NOT_SET - IMPS initiate now sends encryptedTpin instead of encryptedMpin; handles INVALID_TPIN and TPIN_NOT_SET - Share one RSA-OAEP encryption and key-retry path for MPIN and TPIN - Forgot MPIN on sign-in and Reset MPIN in the Security Center - Fund transfer history list and detail, reachable from Home - Brand logo on sign-in and palette updates Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
abb14d5768
commit
f1def81eb7
@@ -11,7 +11,7 @@ import { biometrics } from '../../../services/security/biometrics';
|
||||
import { authActions } from '../store/authSlice';
|
||||
import { selectRegistration } from '../store/authSelectors';
|
||||
import { completeMpinSetup, rememberRegisteredProfile } from '../store/authThunk';
|
||||
import { MPIN_LENGTH, describeMpinProblem, isValidMpin } from '../utils/authValidation';
|
||||
import { MPIN_LENGTH, describeMpinProblem, isAcceptableNewMpin } from '../utils/authValidation';
|
||||
import { CodeInput } from '../components/CodeInput';
|
||||
import type { AuthStackParamList } from '../../../navigation/types';
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function CreateMpinScreen({ navigation }: Props) {
|
||||
: undefined;
|
||||
|
||||
const canSubmit =
|
||||
isValidMpin(mpin) && confirmMpin === mpin && !isLoading;
|
||||
isAcceptableNewMpin(mpin) && confirmMpin === mpin && !isLoading;
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!canSubmit) {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
|
||||
import { ScreenShell } from '../../../components';
|
||||
import { useAppSelector } from '../../../app/hooks';
|
||||
import { env } from '../../../config/env';
|
||||
import { FeatureKey } from '../../../constants/featureKeys';
|
||||
import { useFeature } from '../../../features';
|
||||
import type { AuthStackParamList } from '../../../navigation/types';
|
||||
import { selectLastProfile } from '../store/authSelectors';
|
||||
import { MpinResetFlow } from '../components/MpinResetFlow';
|
||||
|
||||
type Props = Readonly<NativeStackScreenProps<AuthStackParamList, 'ForgotMpin'>>;
|
||||
|
||||
/** Forgot MPIN from sign-in: signed out, so success simply returns there. */
|
||||
export default function ForgotMpinScreen({ navigation }: Props) {
|
||||
const lastProfile = useAppSelector(selectLastProfile);
|
||||
const registrationEnabled = useFeature(FeatureKey.AUTH_REGISTER);
|
||||
|
||||
return (
|
||||
<ScreenShell
|
||||
title="Forgot MPIN"
|
||||
subtitle="Reset it with a code sent to your registered mobile"
|
||||
withTabBar={false}
|
||||
contentContainerStyle={styles.container}
|
||||
>
|
||||
<MpinResetFlow
|
||||
orgId={lastProfile?.orgId ?? env.orgId}
|
||||
initialMobile={lastProfile?.mobileNumber}
|
||||
onSuccess={() => navigation.navigate('Login')}
|
||||
onMpinNotSet={registrationEnabled ? () => navigation.navigate('Register') : undefined}
|
||||
/>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 16,
|
||||
flexGrow: 1,
|
||||
},
|
||||
});
|
||||
@@ -86,6 +86,7 @@ export default function LoginScreen({ navigation }: Props) {
|
||||
|
||||
const userRoleEnabled = useFeature(FeatureKey.AUTH_LOGIN_USER_ROLE);
|
||||
const agentRoleEnabled = useFeature(FeatureKey.AUTH_LOGIN_AGENT_ROLE);
|
||||
const forgotMpinEnabled = useFeature(FeatureKey.AUTH_LOGIN_FORGOT_PASSWORD);
|
||||
|
||||
const isLoading = signInStatus === 'loading';
|
||||
|
||||
@@ -420,6 +421,7 @@ export default function LoginScreen({ navigation }: Props) {
|
||||
{!isReturningCustomer ? (
|
||||
<Input
|
||||
label="Registered mobile number"
|
||||
onBrand
|
||||
leftAccessory={<SimIcon size={18} color={theme.colors.textSecondary} />}
|
||||
prefix="+91"
|
||||
keyboardType="number-pad"
|
||||
@@ -434,6 +436,7 @@ export default function LoginScreen({ navigation }: Props) {
|
||||
|
||||
<CodeInput
|
||||
label="Enter MPIN"
|
||||
onBrand
|
||||
value={mpin}
|
||||
onChangeText={handleMpinChange}
|
||||
length={MPIN_LENGTH}
|
||||
@@ -540,14 +543,25 @@ export default function LoginScreen({ navigation }: Props) {
|
||||
</Text>
|
||||
</FeatureGate>
|
||||
|
||||
{/* There is no forgot-MPIN or reset flow in the API — by policy,
|
||||
not by omission — so the app says so instead of offering a
|
||||
dead link. */}
|
||||
<Text
|
||||
style={[theme.text.caption, styles.centreText, { color: theme.colors.onBrandMuted }]}
|
||||
>
|
||||
Forgot your MPIN? Visit your branch to have it reset.
|
||||
</Text>
|
||||
{/* Customers only — an agent's credentials are the bank's to
|
||||
reset. Switching the feature off falls back to the branch. */}
|
||||
{activeRole === 'User' ? (
|
||||
forgotMpinEnabled ? (
|
||||
<Text
|
||||
accessibilityRole="link"
|
||||
onPress={() => navigation.navigate('ForgotMpin')}
|
||||
style={[theme.text.bodySmall, styles.centreText, { color: theme.colors.onBrandPrimary }]}
|
||||
>
|
||||
Forgot MPIN?
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
style={[theme.text.caption, styles.centreText, { color: theme.colors.onBrandMuted }]}
|
||||
>
|
||||
Forgot your MPIN? Visit your branch to have it reset.
|
||||
</Text>
|
||||
)
|
||||
) : null}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
@@ -13,9 +13,10 @@ import type { AuthStackParamList } from '../../../navigation/types';
|
||||
type Props = Readonly<NativeStackScreenProps<AuthStackParamList, 'MpinLocked'>>;
|
||||
|
||||
/**
|
||||
* A locked profile is a dead end by policy: there is no forgot-MPIN flow and no
|
||||
* in-app reset anywhere in the API. Saying so plainly beats a retry button that
|
||||
* can only ever fail.
|
||||
* A locked profile is a dead end by policy. The forgot-MPIN flow is for a
|
||||
* customer who does not know their MPIN; whether a reset also lifts a lock is
|
||||
* the bank's rule, not the app's, so it is not offered here. Saying so plainly
|
||||
* beats a retry button that can only ever fail.
|
||||
*/
|
||||
export default function MpinLockedScreen({ navigation }: Props) {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useTheme } from '../../../theme';
|
||||
import { useApiErrorToast } from '../../../hooks/useApiErrorToast';
|
||||
import { isValidPhone } from '../../../utils/validation';
|
||||
import { simCard, type SimInfo, type SimSubscription } from '../../../services/device/simCard';
|
||||
import { BrandLogo } from '../components/BrandLogo';
|
||||
import { SimChooser } from '../components/SimChooser';
|
||||
import { authActions } from '../store/authSlice';
|
||||
import { selectLastProfile, selectRegistration } from '../store/authSelectors';
|
||||
@@ -422,20 +423,7 @@ export default function RegisterScreen({ navigation }: Props) {
|
||||
|
||||
{/* ── Brand and purpose ──────────────────────────────────────────── */}
|
||||
<Animated.View entering={FadeInDown.duration(420)} style={styles.heroBody}>
|
||||
<View style={styles.brandRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.mark,
|
||||
{
|
||||
backgroundColor: theme.colors.onBrandSurfaceStrong,
|
||||
borderRadius: theme.radius.sm,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons name="shield-checkmark" size={26} color={theme.colors.onBrandPrimary} />
|
||||
</View>
|
||||
<Text style={[theme.text.h1, { color: theme.colors.onBrandPrimary }]}>BankCBS</Text>
|
||||
</View>
|
||||
<BrandLogo width={270} />
|
||||
|
||||
<Text
|
||||
accessibilityRole="header"
|
||||
@@ -648,17 +636,6 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
heroBody: { gap: 10 },
|
||||
brandRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 9,
|
||||
},
|
||||
mark: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
heroTitle: {
|
||||
marginTop: 4,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user