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,56 @@
## Context

The initial recovery-key reminder is shown from `src/screens/Home/HomeScreen.tsx` whenever the current Keeper app instance has not been marked as backed up. Today the modal only offers a continue path into `ViewRecoveryKeyScreen`, and `src/screens/BackupWallet/ViewRecoveryKeyScreen.tsx` only exits cleanly after the user completes the confirmation challenge and dispatches backup actions.

This feature adds a controlled deferral path for the software hot key onboarding flow. The solution must keep the existing backup-confirmation behavior intact, avoid mutating backup state on skip, and make the risk of skipping explicit to the user.

## Goals / Non-Goals

**Goals:**
- Add a skip action to the home recovery-key modal itself.
- Require explicit risk acknowledgement before the skip action is enabled.
- Let the user continue into Home or leave the recovery-key screen without dispatching backup confirmation actions.
- Keep the existing confirmed-backup path unchanged.

**Non-Goals:**
- Changing Redux slices, sagas, or backup persistence semantics.
- Adding Realm schema or MMKV storage keys.
- Modifying PSBT, hardware signer, Vault, or cloud backup behavior.

## Decisions

1. **Keep skip state ephemeral to the current modal interaction**
- Rationale: the app already decides whether to show the reminder from `recoveryKeyBackedUpByAppId`; because a skipped backup must not be treated as confirmed, the flow should simply close or navigate home without updating account or BHR state.
- Alternative considered: persist a separate “reminded” or “skipped” flag. Rejected because it changes product behavior beyond the request and would require store/storage changes plus migration work.

2. **Embed the risk checkbox inside the existing home modal content**
- Rationale: `KeeperModal` already supports custom `Content` and a secondary action, so the least invasive implementation is to reuse that structure and gate the secondary “Skip” action on a checkbox acknowledgement.
- Alternative considered: build a second confirmation modal after tapping skip. Rejected because the issue asks for the skip option in the home modal itself.

3. **Support skip from `ViewRecoveryKeyScreen` via navigation-only exit**
- Rationale: users who continue into the recovery screen should still be able to back out safely without marking the recovery key as backed up. This keeps the “use the wallet without creating a hot key backup now” path consistent with the request.
- Alternative considered: leave the recovery screen mandatory once opened. Rejected because it still traps the user after they choose to defer.

4. **Localize the new warning and acknowledgement copy**
- Rationale: the modal already uses translation strings, so the new risk text and skip acknowledgement should live alongside existing home/backup translations.

Redux slice(s) / saga(s) involved: none changed. Existing `account` and `bhr` actions remain only on the confirm-backup path.

PSBT / hardware interaction: none.

Realm schema / MMKV additions: none.

Affected files:
- `src/screens/Home/HomeScreen.tsx`
- `src/screens/BackupWallet/ViewRecoveryKeyScreen.tsx`
- `src/context/Localization/language/en.json`
- `src/context/Localization/language/es.json`
- Focused tests covering the modal and skip behavior in the affected screen test area

Migration needed: none; `src/store/migrations.ts` is unchanged because store shape does not change.

## Risks / Trade-offs

- **[Risk]** Users may skip without understanding the consequence. → **Mitigation:** require a checkbox acknowledgement and show explicit warning copy in the modal before enabling skip.
- **[Risk]** Navigation changes could accidentally mark backup complete. → **Mitigation:** keep all backup-related dispatches only on the existing confirm path and use navigation-only exits for skip flows.
- **[Risk]** The reminder may continue to appear after skip, which could feel repetitive. → **Mitigation:** accept this as the correct behavior for now because the issue requests deferral, not dismissal of the backup requirement.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Why

The home screen currently blocks the initial recovery key flow until the user completes the confirmation challenge, which prevents users from entering the app if they are not ready to back up their hot key immediately. We need a safer deferral path that still communicates the risk clearly and requires an explicit acknowledgement before allowing the user to skip.

## What Changes

- Add a skip action to the initial home screen recovery key modal so users can defer the first recovery key confirmation.
- Require users to actively acknowledge the backup risk with a checkbox before the skip action is enabled.
- Allow skipped users to continue into the wallet without marking the recovery key as backed up, so the recovery key flow can still be completed later.
- Update the recovery key confirmation screen to support a skip exit path that returns users to Home without falsely confirming backup state.

## Capabilities

### New Capabilities
- `recovery-key-confirmation-skip`: Lets a user defer the initial recovery key confirmation after explicitly acknowledging the risk in the home screen modal.

### Modified Capabilities
- None.

## Impact

- Affects both mainnet and testnet environments because the home screen and hot wallet backup flow are shared.
- No hardware signer compatibility impact; this only changes the software hot key recovery-key onboarding path.
- No subscription tier gating changes.
- Security/privacy impact: no new key material, storage location, or network call is introduced, but the UI must clearly communicate that skipping increases recovery risk if the device is lost before backup.
- Expected code areas: `src/screens/Home/HomeScreen.tsx`, `src/screens/BackupWallet/ViewRecoveryKeyScreen.tsx`, localization strings, and focused tests for the modal/skip behavior.

## Non-goals

- Changing how the recovery key is generated, stored, or encrypted.
- Marking a skipped recovery key as backed up.
- Altering hardware signer, Vault, or cloud backup flows outside the initial hot key confirmation UX.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## ADDED Requirements

### Requirement: User can defer the initial recovery key confirmation from Home
The system SHALL show a skip option in the initial Home recovery-key modal for a software-key `Wallet`, and the skip action MUST remain disabled until the user explicitly acknowledges the recovery risk.

#### Scenario: Skip becomes available after risk acknowledgement
- **GIVEN** the current Keeper app instance has not been marked as backed up and the initial recovery-key modal is shown on Home
- **WHEN** the user reads the risk warning and checks the acknowledgement box
- **THEN** the modal SHALL enable the skip action so the user can continue using the wallet without confirming the recovery key in that session

#### Scenario: Skip stays blocked without acknowledgement
- **GIVEN** the current Keeper app instance has not been marked as backed up and the initial recovery-key modal is shown on Home
- **WHEN** the user has not checked the acknowledgement box
- **THEN** the system MUST keep the skip action disabled and MUST NOT mark the recovery key as backed up

### Requirement: Skipping MUST NOT confirm recovery-key backup state
When a user skips from the initial Home modal or exits the recovery-key confirmation screen, the system SHALL return the user to Home without dispatching the backup-confirmed actions for the software-key `Wallet`.

#### Scenario: Skip from Home leaves backup pending
- **GIVEN** the initial recovery-key modal is shown on Home for a Keeper app instance whose recovery key is still pending backup
- **WHEN** the user checks the acknowledgement box and taps skip
- **THEN** the system SHALL close the modal and keep the recovery-key backup state pending for that app instance

#### Scenario: Skip from recovery-key screen avoids false confirmation
- **GIVEN** the user has navigated to the recovery-key confirmation screen from Home
- **WHEN** the user chooses to skip instead of finishing the confirmation challenge
- **THEN** the system SHALL return the user to Home and MUST NOT dispatch backup confirmation or cloud-backup actions
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## 1. UI Components

- [x] 1.1 Update the Home recovery-key modal to show risk copy, an acknowledgement checkbox, and a gated skip action.
- [x] 1.2 Update the recovery-key confirmation screen to allow skipping back to Home without confirming backup.

## 2. Business Logic / Hooks

- [x] 2.1 Keep skip behavior navigation-only so the recovery key remains pending until the existing confirm path completes.

## 3. Store (Slice + Saga)

- [x] 3.1 Confirm no Redux slice, saga, or migration changes are needed because skip does not mutate backup state.

## 4. Storage

- [x] 4.1 Confirm no Realm, MMKV, or keychain storage changes are needed for the skip flow.

## 5. Tests

- [x] 5.1 Add or update focused tests covering the gated skip action and the recovery-screen skip path.
31 changes: 31 additions & 0 deletions openspec/specs/recovery-key-confirmation-skip/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# recovery-key-confirmation-skip Specification

## Purpose
TBD - created by archiving change add-initial-recovery-key-skip-option. Update Purpose after archive.
## Requirements
### Requirement: User can defer the initial recovery key confirmation from Home
The system SHALL show a skip option in the initial Home recovery-key modal for a software-key `Wallet`, and the skip action MUST remain disabled until the user explicitly acknowledges the recovery risk.

#### Scenario: Skip becomes available after risk acknowledgement
- **GIVEN** the current Keeper app instance has not been marked as backed up and the initial recovery-key modal is shown on Home
- **WHEN** the user reads the risk warning and checks the acknowledgement box
- **THEN** the modal SHALL enable the skip action so the user can continue using the wallet without confirming the recovery key in that session

#### Scenario: Skip stays blocked without acknowledgement
- **GIVEN** the current Keeper app instance has not been marked as backed up and the initial recovery-key modal is shown on Home
- **WHEN** the user has not checked the acknowledgement box
- **THEN** the system MUST keep the skip action disabled and MUST NOT mark the recovery key as backed up

### Requirement: Skipping MUST NOT confirm recovery-key backup state
When a user skips from the initial Home modal or exits the recovery-key confirmation screen, the system SHALL return the user to Home without dispatching the backup-confirmed actions for the software-key `Wallet`.

#### Scenario: Skip from Home leaves backup pending
- **GIVEN** the initial recovery-key modal is shown on Home for a Keeper app instance whose recovery key is still pending backup
- **WHEN** the user checks the acknowledgement box and taps skip
- **THEN** the system SHALL close the modal and keep the recovery-key backup state pending for that app instance

#### Scenario: Skip from recovery-key screen avoids false confirmation
- **GIVEN** the user has navigated to the recovery-key confirmation screen from Home
- **WHEN** the user chooses to skip instead of finishing the confirmation challenge
- **THEN** the system SHALL return the user to Home and MUST NOT dispatch backup confirmation or cloud-backup actions

11 changes: 7 additions & 4 deletions src/components/KeeperModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type ModalProps = {
loading?: boolean;
secondaryIcon?: any;
disable?: boolean;
secondaryDisable?: boolean;
};

function KeeperModal(props: ModalProps) {
Expand Down Expand Up @@ -76,6 +77,7 @@ function KeeperModal(props: ModalProps) {
loading = false,
secondaryIcon = null,
disable = false,
secondaryDisable = false,
} = props;
const subTitleColor = ignored || textColor;
const { bottom } = useSafeAreaInsets();
Expand Down Expand Up @@ -180,10 +182,11 @@ function KeeperModal(props: ModalProps) {
primaryTextColor={
buttonTextColor == 'buttonText' ? `${colorMode}.buttonText` : buttonTextColor
}
secondaryCallback={secondaryCallback}
secondaryText={secondaryButtonText}
SecondaryIcon={secondaryIcon}
secondaryTextColor={
secondaryCallback={secondaryCallback}
secondaryText={secondaryButtonText}
secondaryDisable={secondaryDisable}
SecondaryIcon={secondaryIcon}
secondaryTextColor={
secButtonTextColor == 'headerText'
? `${colorMode}.textGreen`
: secButtonTextColor
Expand Down
6 changes: 4 additions & 2 deletions src/context/Localization/language/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,9 @@
"securityTipDesc": "Recreate the multisig on more coordinators. Receive a small amount and send a part of it. Check the balances are appropriately reflected across all the coordinators after each step.",
"backupModalTitle": "Confirm your Recovery Key",
"backupModalSubTitle": "To make sure your Recovery Key is not lost, you need to confirm it.",
"backupModalDesc": "Keeper keeps a protected copy of your Keeper data, encrypted using your Recovery Key, to help with app recovery. Keeper cannot read this data."
"backupModalDesc": "Keeper keeps a protected copy of your Keeper data, encrypted using your Recovery Key, to help with app recovery. Keeper cannot read this data.",
"backupModalRiskDesc": "Skipping this step means you may lose access to this wallet if this device is lost or damaged before you back up the Recovery Key.",
"backupModalSkipAcknowledge": "I understand the risks and want to skip Recovery Key confirmation for now."
},
"transactions": {
"Fees": "Fees",
Expand Down Expand Up @@ -2135,4 +2137,4 @@
"importInfoDesc2": "Once imported, you’ll be able to access and manage your USDT without needing TRX.",
"importInfoDescNote": "Note: Standard TRC-20 wallets that require TRX to move funds are not supported."
}
}
}
6 changes: 4 additions & 2 deletions src/context/Localization/language/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,9 @@
"securityTipDesc": "Recreate the multisig on more coordinators. Receive a small amount and send a part of it. Check the balances are appropriately reflected across all the coordinators after each step.",
"backupModalTitle": "Confirm your Recovery Key",
"backupModalSubTitle": "To make sure your Recovery Key is not lost, you need to confirm it.",
"backupModalDesc": "Keeper keeps a protected copy of your Keeper data, encrypted using your Recovery Key, to help with app recovery. Keeper cannot read this data."
"backupModalDesc": "Keeper keeps a protected copy of your Keeper data, encrypted using your Recovery Key, to help with app recovery. Keeper cannot read this data.",
"backupModalRiskDesc": "Skipping this step means you may lose access to this wallet if this device is lost or damaged before you back up the Recovery Key.",
"backupModalSkipAcknowledge": "I understand the risks and want to skip Recovery Key confirmation for now."
},
"transactions": {
"Fees": "Fees",
Expand Down Expand Up @@ -2135,4 +2137,4 @@
"importInfoDesc2": "Once imported, you’ll be able to access and manage your USDT without needing TRX.",
"importInfoDescNote": "Note: Standard TRC-20 wallets that require TRX to move funds are not supported."
}
}
}
24 changes: 15 additions & 9 deletions src/screens/BackupWallet/ViewRecoveryKeyScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ export const ViewRecoveryKeyScreen = ({ navigation }) => {
const { backupAllFailure, backupAllSuccess } = useAppSelector((state) => state.bhr);
const [title, setTitle] = useState(homeTxt.backupModalTitle);

const navigateHome = () =>
navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: 'Home' }],
})
);

useEffect(() => {
if (backupAllSuccess || backupAllFailure) {
dispatch(setBackupAllSuccess(false));
Expand Down Expand Up @@ -114,7 +122,12 @@ export const ViewRecoveryKeyScreen = ({ navigation }) => {
keyExtractor={(item) => item}
/>
</Box>
<Buttons primaryText={common.confirm} primaryCallback={() => setConfirmSeedModal(true)} />
<Buttons
primaryText={common.confirm}
primaryCallback={() => setConfirmSeedModal(true)}
secondaryText={common.skip}
secondaryCallback={navigateHome}
/>
</Box>
<Box>
<ModalWrapper
Expand Down Expand Up @@ -152,14 +165,7 @@ export const ViewRecoveryKeyScreen = ({ navigation }) => {
textColor={`${colorMode}.textGreen`}
subTitleColor={`${colorMode}.modalSubtitleBlack`}
buttonText={'Finish'}
buttonCallback={() =>
navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: 'Home' }],
})
)
}
buttonCallback={navigateHome}
/>
<ActivityIndicatorView visible={loader} />
</Box>
Expand Down
46 changes: 44 additions & 2 deletions src/screens/Home/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { StyleSheet } from 'react-native';
import { Box, useColorMode } from '@gluestack-ui/themed-native-base';
import { Box, Checkbox, useColorMode } from '@gluestack-ui/themed-native-base';
import React, { useContext, useEffect, useState } from 'react';
import useWallets from 'src/hooks/useWallets';
import { useAppSelector } from 'src/store/hooks';
Expand Down Expand Up @@ -37,6 +37,7 @@ function NewHomeScreen({ route }) {
const { wallets } = useWallets({ getAll: true });
const [electrumErrorVisible, setElectrumErrorVisible] = useState(false);
const [backupModalVisible, setBackupModalVisible] = useState(true);
const [acceptedBackupRisk, setAcceptedBackupRisk] = useState(false);
const home_header_circle_background = ThemedColor({ name: 'home_header_circle_background' });

const { relayWalletUpdate, relayWalletError, realyWalletErrorMessage, homeToastMessage } =
Expand All @@ -61,6 +62,7 @@ function NewHomeScreen({ route }) {
useEffect(() => {
if (shouldShowBackupModal) {
setBackupModalVisible(true);
setAcceptedBackupRisk(false);
}
}, [shouldShowBackupModal]);

Expand Down Expand Up @@ -192,10 +194,37 @@ function NewHomeScreen({ route }) {
<Text color={`${colorMode}.primaryText`} style={{ fontSize: 14, letterSpacing: 0.13 }}>
{homeTranslation.backupModalDesc}
</Text>
<Text color={`${colorMode}.primaryText`} style={{ fontSize: 14, letterSpacing: 0.13 }}>
{homeTranslation.backupModalRiskDesc}
</Text>
<Box style={styles.checkboxContainer}>
<Checkbox
testID="checkbox_backup_skip_acknowledgement"
value="acceptBackupRisk"
isChecked={acceptedBackupRisk}
onChange={(isChecked) => setAcceptedBackupRisk(isChecked)}
accessibilityLabel="acceptBackupRisk"
_checked={{
bg: `${colorMode}.pantoneGreen`,
borderColor: `${colorMode}.pantoneGreen`,
_icon: {
color: 'white',
},
}}
/>
<Text color={`${colorMode}.primaryText`} style={styles.checkboxText}>
{homeTranslation.backupModalSkipAcknowledge}
</Text>
</Box>
</Box>
);
};

const closeBackupModal = () => {
setBackupModalVisible(false);
setAcceptedBackupRisk(false);
};

return (
<Box backgroundColor={`${colorMode}.primaryBackground`} style={styles.container}>
<InititalAppController
Expand Down Expand Up @@ -223,8 +252,11 @@ function NewHomeScreen({ route }) {
buttonBackground={`${colorMode}.pantoneGreen`}
showCloseIcon={false}
buttonText={common.continue}
secondaryButtonText={common.skip}
secondaryDisable={!acceptedBackupRisk}
secondaryCallback={closeBackupModal}
buttonCallback={() => {
setBackupModalVisible(false);
closeBackupModal();
setTimeout(() => {
navigation.dispatch(
CommonActions.reset({
Expand Down Expand Up @@ -252,4 +284,14 @@ const styles = StyleSheet.create({
alignItems: 'center',
paddingTop: hp(22),
},
checkboxContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: wp(12),
},
checkboxText: {
flex: 1,
fontSize: 13,
letterSpacing: 0.13,
},
});
Loading