Skip to content

Commit d384c0c

Browse files
OtavioStasiakclaude
andcommitted
feat(biometric-trust): show explanatory subtitle on enrollment-change unlock
When handleLocalAuthentication invalidates biometric trust because the device enrolment set changed, the passcode modal now displays an explanatory subtitle reading "Biometric enrollment changed, please use your passcode". The signal travels over LOCAL_AUTHENTICATE_EMITTER's existing reason payload (added in the previous commit). PasscodeEnter reads reason from props, mirrors it into local state so a button-triggered invalidation can update it without re-emitting, and renders Base's subtitle slot only when reason === 'enrollmentChanged'. The subtitle clears naturally on the next modal open because reason is reinitialised from props each session. Normal auto-lock unlocks, cancel/error fallbacks, and re-opens after a successful unlock leave the subtitle hidden — it is strictly tied to the invalidation event. Part of VLN-216. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 3a59438 commit d384c0c

2 files changed

Lines changed: 24 additions & 3 deletions

File tree

app/containers/Passcode/PasscodeEnter.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,23 @@ describe('PasscodeEnter biometry button', () => {
9292
expect(getByTestId('biometry-button')).toBeTruthy();
9393
});
9494
});
95+
96+
describe('PasscodeEnter enrollmentChanged subtitle', () => {
97+
beforeEach(() => {
98+
jest.clearAllMocks();
99+
});
100+
101+
it('renders explanatory subtitle when reason === "enrollmentChanged"', () => {
102+
const { getByText } = render(
103+
<PasscodeEnter hasBiometry={false} skipAutoBiometry reason='enrollmentChanged' finishProcess={jest.fn()} />
104+
);
105+
106+
expect(getByText('Local_authentication_biometric_enrollment_changed')).toBeTruthy();
107+
});
108+
109+
it('does not render subtitle when reason is undefined', () => {
110+
const { queryByText } = render(<PasscodeEnter hasBiometry={false} skipAutoBiometry finishProcess={jest.fn()} />);
111+
112+
expect(queryByText('Local_authentication_biometric_enrollment_changed')).toBeNull();
113+
});
114+
});

app/containers/Passcode/PasscodeEnter.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ const PasscodeEnter = ({
3636
// biometry button immediately hides the button within the same modal session, without
3737
// re-emitting LOCAL_AUTHENTICATE_EMITTER (which would orphan the upstream openModal promise).
3838
const [hasBiometry, setHasBiometry] = useState<boolean>(initialHasBiometry);
39-
// `reason` is held for slice 03's subtitle copy; _reason intentionally unused for now.
40-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
41-
const [_reason, setReason] = useState<BiometricInvalidationReason | undefined>(initialReason);
39+
const [reason, setReason] = useState<BiometricInvalidationReason | undefined>(initialReason);
4240
const { setItem: setAttempts } = useAsyncStorage(ATTEMPTS_KEY);
4341
const { setItem: setLockedUntil } = useAsyncStorage(LOCKED_OUT_TIMER_KEY);
4442

@@ -103,11 +101,14 @@ const PasscodeEnter = ({
103101
return <Locked setStatus={setStatus} />;
104102
}
105103

104+
const subtitle = reason === 'enrollmentChanged' ? I18n.t('Local_authentication_biometric_enrollment_changed') : null;
105+
106106
return (
107107
<Base
108108
ref={ref}
109109
type={TYPE.ENTER}
110110
title={I18n.t('Passcode_enter_title')}
111+
subtitle={subtitle}
111112
showBiometry={hasBiometry}
112113
onEndProcess={onEndProcess}
113114
onBiometryPress={biometry}

0 commit comments

Comments
 (0)