Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-11
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## Context

The login screen and passcode creation flow both render the shared `KeyPadView` / `KeyPadButton` components from `src/components/AppNumPad/`. The reported failure is intermittent: one numeric label can render late or disappear even though the keypad layout remains visible and tappable. This points to a presentation-layer issue inside the shared keypad button rather than Redux, saga, Realm, MMKV, PSBT, or hardware signer flows.

## Goals / Non-Goals

**Goals:**
- Make keypad digit labels render consistently on first paint in shared passcode flows.
- Keep the fix isolated to the shared keypad presentation components.
- Add focused tests that verify the shared keypad renders all digits.

**Non-Goals:**
- Changing any Redux slice or saga; none are involved in this UI-only fix.
- Changing passcode validation, biometric authentication, or navigation.
- Adding Realm schema changes, MMKV keys, or migrations; none are needed.

## Decisions

1. **Stabilize the shared digit label rendering in `KeyPadButton`.**
The keypad is reused by login and passcode creation, so fixing the shared button prevents duplicate screen-level work. The implementation should prefer the most direct React Native text rendering path for the digit label and keep the existing touch/animation behavior intact.

- Alternative considered: patching layout in `Login.tsx` only. Rejected because `CreatePin.tsx` uses the same shared keypad and could retain the bug.
- Alternative considered: changing passcode state timing or throttling. Rejected because the screenshot shows a display problem before any input interaction.

2. **Cover the regression with a focused component test.**
A keypad-level test can assert that digits `0` through `9` are present without coupling the test to login business logic.

3. **Avoid store and persistence changes.**
Redux slices/sagas involved: none. Realm schema changes: none. MMKV additions: none. Migration in `src/store/migrations.ts`: not required.

## Risks / Trade-offs

- **[Risk]** Replacing the label rendering path could slightly change keypad typography.
**Mitigation:** Keep the existing font size, line height, and alignment so the visual change stays minimal.

- **[Risk]** Tests may need mocks for shared animated/themed wrappers.
**Mitigation:** Reuse the repository’s current Jest setup and keep the assertion focused on rendered digit labels.

## Migration Plan

No data migration or rollout sequencing is required. The change is a local UI rendering fix that can be rolled back by reverting the shared keypad component if needed.

## Open Questions

- None at this time.

## Affected Files

- Modified: `src/components/AppNumPad/KeyPadButton.tsx`
- Modified or added test: keypad-focused test file under the existing Jest test structure
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Why

The passcode keypad on the login screen intermittently renders with a missing digit, which blocks or delays passcode entry for returning users. This needs to be fixed now because the issue affects a core unlock flow on both mainnet and testnet environments.

## What Changes

- Stabilize the passcode keypad digit rendering on the login and passcode creation flows so all digits remain visible as soon as the keypad appears.
- Add focused validation around the keypad digit labels to guard against regressions in the shared keypad component.
- Keep the change limited to keypad presentation behavior without altering passcode verification, storage, or signer flows.

## Capabilities

### New Capabilities
- `passcode-keypad-rendering`: Ensures the shared passcode keypad consistently displays all numeric keys in authentication flows.

### Modified Capabilities
- None.

## Impact

- Affected code: `src/components/AppNumPad/*`, login/passcode screens that render the shared keypad, and focused tests covering keypad labels.
- APIs/dependencies: No external API or dependency changes.
- Hardware signer compatibility: No impact; this change only affects local passcode UI rendering.
- Subscription gating: No subscription tier impact.
- Security/privacy impact: No key material, storage, or network behavior changes; the update is limited to local UI rendering.

## Non-goals

- Changing passcode length, passcode validation, or biometric authentication behavior.
- Changing wallet, vault, signer, or network connectivity flows.
- Introducing new theming, navigation, or subscription behavior unrelated to keypad visibility.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## ADDED Requirements

### Requirement: Shared passcode keypad renders all numeric keys
The system SHALL render all numeric keys from `0` through `9` when the shared passcode keypad is shown in authentication flows that use `KeyPadView`.

#### Scenario: Login keypad loads with every digit visible
- **GIVEN** the user opens the login screen on mainnet or testnet
- **WHEN** the shared passcode keypad is rendered
- **THEN** numeric keys `1` through `9` and `0` MUST all be visible without waiting for a delayed re-render

#### Scenario: Passcode creation keypad reuses the same stable rendering
- **GIVEN** the user opens the passcode creation flow
- **WHEN** the shared passcode keypad is rendered
- **THEN** numeric keys `1` through `9` and `0` MUST all be visible in the initial keypad layout

#### Scenario: Rendering fix does not change passcode controls
- **GIVEN** an authentication flow renders the shared passcode keypad
- **WHEN** the keypad is displayed
- **THEN** the delete control MUST remain available
- **AND** the keypad MUST continue to emit the same numeric key values when pressed
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## 1. UI Components

- [x] 1.1 Inspect the shared keypad button rendering path and apply the smallest fix that keeps all numeric labels visible on initial render.

## 2. Tests

- [x] 2.1 Add or update a focused Jest test that verifies the shared keypad renders digits `0` through `9`.

## 3. Validation

- [x] 3.1 Install project dependencies and run focused validation for the keypad component and related linting.
- [x] 3.2 Capture a screenshot or equivalent UI evidence showing the keypad digits render after the fix.
23 changes: 23 additions & 0 deletions openspec/specs/passcode-keypad-rendering/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# passcode-keypad-rendering Specification

## Purpose
Define the shared passcode keypad requirement so authentication flows always render visible numeric keys `0` through `9` and retain the existing delete control behavior.
## Requirements
### Requirement: Shared passcode keypad renders all numeric keys
The system SHALL render all numeric keys from `0` through `9` when the shared passcode keypad is shown in authentication flows that use `KeyPadView`.

#### Scenario: Login keypad loads with every digit visible
- **GIVEN** the user opens the login screen on mainnet or testnet
- **WHEN** the shared passcode keypad is rendered
- **THEN** numeric keys `1` through `9` and `0` MUST all be visible without waiting for a delayed re-render

#### Scenario: Passcode creation keypad reuses the same stable rendering
- **GIVEN** the user opens the passcode creation flow
- **WHEN** the shared passcode keypad is rendered
- **THEN** numeric keys `1` through `9` and `0` MUST all be visible in the initial keypad layout

#### Scenario: Rendering fix does not change passcode controls
- **GIVEN** an authentication flow renders the shared passcode keypad
- **WHEN** the keypad is displayed
- **THEN** the delete control MUST remain available
- **AND** the keypad MUST continue to emit the same numeric key values when pressed
13 changes: 7 additions & 6 deletions src/components/AppNumPad/KeyPadButton.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { StyleSheet, TouchableOpacity, Animated } from 'react-native';
import { StyleSheet, TouchableOpacity, Animated, Text as NativeText } from 'react-native';
import React, { useState } from 'react';
import Text from 'src/components/KeeperText';
import ScaleSpring from '../Animations/ScaleSpring';
import ThemedColor from '../ThemedColor/ThemedColor';

export interface Props {
title: string;
onPressNumber: (value: string) => void;
keyColor: string;
// eslint-disable-next-line react/require-default-props
bubbleEffect?: boolean;
}

const KeyPadButton: React.FC<Props> = ({ title, onPressNumber, keyColor, bubbleEffect }: Props) => {
function KeyPadButton({ title, onPressNumber, keyColor, bubbleEffect = false }: Props) {
const [pressed, setPressed] = useState(false);
const keyPad_colors = ThemedColor({ name: 'keyPad_colors' });

Expand Down Expand Up @@ -46,13 +46,13 @@ const KeyPadButton: React.FC<Props> = ({ title, onPressNumber, keyColor, bubbleE
/>
)}

<Text style={styles.keyPadElementText} color={keyColor}>
<NativeText allowFontScaling={false} style={[styles.keyPadElementText, { color: keyColor }]}>
{title}
</Text>
</NativeText>
</TouchableOpacity>
</ScaleSpring>
);
};
}

const styles = StyleSheet.create({
keyPadElementTouchable: {
Expand All @@ -64,6 +64,7 @@ const styles = StyleSheet.create({
keyPadElementText: {
fontSize: 25,
lineHeight: 30,
textAlign: 'center',
zIndex: 1,
opacity: 1,
},
Expand Down
46 changes: 46 additions & 0 deletions tests/components/KeyPadView.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react-native';
import { View } from 'react-native';
import KeyPadView from 'src/components/AppNumPad/KeyPadView';

jest.mock('src/components/Animations/ScaleSpring', () => ({ children }: React.PropsWithChildren) => children);
jest.mock('src/components/ThemedColor/ThemedColor', () => jest.fn(() => '#ffffff'));

describe('KeyPadView', () => {
it('renders digits 0 through 9', () => {
const { getByText, getByTestId } = render(
<KeyPadView
onPressNumber={jest.fn()}
onDeletePressed={jest.fn()}
bubbleEffect
ClearIcon={<View />}
/>
);

['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'].forEach((digit) => {
expect(getByText(digit)).toBeTruthy();
});
expect(getByTestId('btn_clear')).toBeTruthy();
});

it('emits numeric values and delete presses', () => {
const onPressNumber = jest.fn();
const onDeletePressed = jest.fn();
const { getByTestId } = render(
<KeyPadView
onPressNumber={onPressNumber}
onDeletePressed={onDeletePressed}
bubbleEffect
ClearIcon={<View />}
/>
);

fireEvent.press(getByTestId('key_1'));
fireEvent.press(getByTestId('key_0'));
fireEvent.press(getByTestId('btn_clear'));

expect(onPressNumber).toHaveBeenNthCalledWith(1, '1');
expect(onPressNumber).toHaveBeenNthCalledWith(2, '0');
expect(onDeletePressed).toHaveBeenCalledTimes(1);
});
});