diff --git a/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/.openspec.yaml b/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/.openspec.yaml new file mode 100644 index 0000000000..81cd71fe0b --- /dev/null +++ b/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-11 diff --git a/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/design.md b/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/design.md new file mode 100644 index 0000000000..20fb1b1b7d --- /dev/null +++ b/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/design.md @@ -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 diff --git a/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/proposal.md b/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/proposal.md new file mode 100644 index 0000000000..11e2237117 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/proposal.md @@ -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. diff --git a/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/specs/passcode-keypad-rendering/spec.md b/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/specs/passcode-keypad-rendering/spec.md new file mode 100644 index 0000000000..17362835eb --- /dev/null +++ b/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/specs/passcode-keypad-rendering/spec.md @@ -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 diff --git a/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/tasks.md b/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/tasks.md new file mode 100644 index 0000000000..c7e2dcab87 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-fix-passcode-keypad-missing-digit/tasks.md @@ -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. diff --git a/openspec/specs/passcode-keypad-rendering/spec.md b/openspec/specs/passcode-keypad-rendering/spec.md new file mode 100644 index 0000000000..8f237d794f --- /dev/null +++ b/openspec/specs/passcode-keypad-rendering/spec.md @@ -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 diff --git a/src/components/AppNumPad/KeyPadButton.tsx b/src/components/AppNumPad/KeyPadButton.tsx index decf9e0b28..828a277d03 100644 --- a/src/components/AppNumPad/KeyPadButton.tsx +++ b/src/components/AppNumPad/KeyPadButton.tsx @@ -1,6 +1,5 @@ -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'; @@ -8,10 +7,11 @@ 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 = ({ 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' }); @@ -46,13 +46,13 @@ const KeyPadButton: React.FC = ({ title, onPressNumber, keyColor, bubbleE /> )} - + {title} - + ); -}; +} const styles = StyleSheet.create({ keyPadElementTouchable: { @@ -64,6 +64,7 @@ const styles = StyleSheet.create({ keyPadElementText: { fontSize: 25, lineHeight: 30, + textAlign: 'center', zIndex: 1, opacity: 1, }, diff --git a/tests/components/KeyPadView.test.tsx b/tests/components/KeyPadView.test.tsx new file mode 100644 index 0000000000..f33d65c537 --- /dev/null +++ b/tests/components/KeyPadView.test.tsx @@ -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( + } + /> + ); + + ['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( + } + /> + ); + + 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); + }); +});