From 2f64962a6a7e21b2425830343ca324733f40a220 Mon Sep 17 00:00:00 2001 From: Jonathan Tzeng Date: Mon, 17 Aug 2026 12:06:20 -0700 Subject: [PATCH 1/3] Fix lint warnings in CreateWalletImportScene Convert the component to the React.FC form the lint rule expects. --- src/components/scenes/CreateWalletImportScene.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/scenes/CreateWalletImportScene.tsx b/src/components/scenes/CreateWalletImportScene.tsx index 5ee9f302db1..ebdb1c1f401 100644 --- a/src/components/scenes/CreateWalletImportScene.tsx +++ b/src/components/scenes/CreateWalletImportScene.tsx @@ -44,7 +44,7 @@ interface Props extends EdgeAppSceneProps<'createWalletImport'> {} const getOptionKey = (pluginId: string, opt: ImportKeyOption): string => `${pluginId}${opt.optionName}` -const CreateWalletImportComponent = (props: Props): React.JSX.Element => { +const CreateWalletImportComponent: React.FC = props => { const { navigation, route } = props const { createWalletList, walletNames, walletSettingValues } = route.params const theme = useTheme() From b6edb476b02463ebe50cf332ca2916eabc8c0ded Mon Sep 17 00:00:00 2001 From: Jonathan Tzeng Date: Mon, 17 Aug 2026 12:08:11 -0700 Subject: [PATCH 2/3] Keep the Import Wallet Next button clear of the keyboard The scene rendered its content, including the Next button, inside a scroll view that did not react to the keyboard, so opening the keyboard to type a seed hid the button behind it. Shrink the scene to the space above the keyboard, keep the button pinned below the scrolling content, and let the key logo flex instead of holding a fixed 2 rem of margin. Drop the keyboard-dismiss steps the maestro import flows needed to reach the button. --- CHANGELOG.md | 1 + .../07-wallets/C000029a-migrate-wallets.yaml | 22 +- maestro/common/import-wallets.yaml | 29 +- .../__snapshots__/BalanceCard.test.tsx.snap | 1 + .../__snapshots__/Buttons.test.tsx.snap | 10 + .../AutoLogoutModal.test.tsx.snap | 1 + .../__snapshots__/LogsModal.test.tsx.snap | 1 + .../PasswordReminderModal.test.tsx.snap | 1 + .../TextInputModal.test.tsx.snap | 2 + .../CreateWalletEditNameScene.test.tsx.snap | 1 + .../CreateWalletImportScene.test.tsx.snap | 1007 +++++++++-------- .../FioAddressListScene.test.tsx.snap | 1 + .../FioAddressRegisterScene.test.tsx.snap | 1 + .../FioAddressSettingsScene.test.tsx.snap | 1 + .../__snapshots__/RequestScene.test.tsx.snap | 1 + .../__snapshots__/SettingsScene.test.tsx.snap | 1 + .../TransactionDetailsScene.test.tsx.snap | 2 + src/components/buttons/ButtonsView.tsx | 15 +- .../scenes/CreateWalletImportScene.tsx | 287 +++-- 19 files changed, 762 insertions(+), 623 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40c67c4be8f..7cf08ed8d41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - fixed: Notification center cards no longer shrink their text to fit. Long titles and messages now truncate with an ellipsis so every card renders at the same size. - fixed: Sort the Privacy Settings Nym Mix Net asset list alphabetically by display name - fixed: Next button overlapping the wallet list on the Choose Wallets to Add scene +- fixed: Keyboard no longer covers the Next button on the Import Wallet scene. The scene ends flush against the keyboard instead of leaving a band of dead space the content cannot scroll into, its spacing no longer stretches or compresses with the length of the seed phrase, and the button sits just above the keyboard rather than 3 rem clear of it. - fixed: Wrap the fiat value in parentheses on the Stake/Unstake/Claim amount row, and remove the space between the fiat symbol and amount to match the network fee tile. - fixed: Staked "locked" balance in the wallet view no longer gets cut off. The crypto amount is truncated to an exchange-rate-appropriate number of decimals, and the text is no longer clamped to a fraction of the card width. - fixed: Improve the unstake error experience by replacing the popup alert and generic "unknown error occurred" with the real error in the scene's error field, and showing a clear message when the wallet lacks the balance to cover the unstaking network fee. diff --git a/maestro/07-wallets/C000029a-migrate-wallets.yaml b/maestro/07-wallets/C000029a-migrate-wallets.yaml index e166e442070..4e601bffc8b 100644 --- a/maestro/07-wallets/C000029a-migrate-wallets.yaml +++ b/maestro/07-wallets/C000029a-migrate-wallets.yaml @@ -96,20 +96,14 @@ tags: - tapOn: Private Key or Private Seed - inputText: ${IMPORT_SEED} -# Drop keyboard - Android -- runFlow: - when: - platform: Android - commands: - - hideKeyboard - # - tapOn: Next # odd additional tap required on android sometimes -# Drop keyboard - iOS -- runFlow: - when: - platform: iOS - commands: - - tapOn: "Private Key or Private Seed" - +# The Import Wallet scene keeps Next clear of the keyboard on both platforms, so +# neither one drops the keyboard to reach it. A long phrase can still push Next +# past the bottom of the scroll viewport, which the scene scrolls to reveal. +- scrollUntilVisible: + element: + text: Next + direction: DOWN + timeout: 20000 - tapOn: Next # Sometimes android requires additional tap diff --git a/maestro/common/import-wallets.yaml b/maestro/common/import-wallets.yaml index 0d6025a3f9a..8738b8b9d6c 100644 --- a/maestro/common/import-wallets.yaml +++ b/maestro/common/import-wallets.yaml @@ -39,29 +39,16 @@ env: - tapOn: Private Key or Private Seed - inputText: ${SEED_PHRASE} -# Drop keyboard - Android -- runFlow: - when: - platform: Android - commands: - - hideKeyboard - - tapOn: "Enter your.*" -# Drop keyboard - iOS -- runFlow: - when: - platform: iOS - commands: - - tapOn: "Private Key or Private Seed" - +# The Import Wallet scene keeps Next clear of the keyboard on both platforms, so +# neither one drops the keyboard to reach it. A long phrase can still push Next +# past the bottom of the scroll viewport, which the scene scrolls to reveal. +- scrollUntilVisible: + element: + text: Next + direction: DOWN + timeout: 20000 - tapOn: Next -# # Sometimes android requires additional tap -# - runFlow: -# when: -# visible: Import Wallet -# commands: -# - tapOn: Next - # Add birthday height for Zcash and Pirate Chain # Index for targetting the correct "edit" icon - evalScript: ${var index = 0} diff --git a/src/__tests__/components/__snapshots__/BalanceCard.test.tsx.snap b/src/__tests__/components/__snapshots__/BalanceCard.test.tsx.snap index 44692fd6ff0..034df1b0279 100644 --- a/src/__tests__/components/__snapshots__/BalanceCard.test.tsx.snap +++ b/src/__tests__/components/__snapshots__/BalanceCard.test.tsx.snap @@ -1505,6 +1505,7 @@ exports[`BalanceCard should render with loading props 1`] = ` @@ -319,15 +359,31 @@ exports[`CreateWalletImportScene should render with loading props 1`] = ` /> @@ -364,142 +420,48 @@ exports[`CreateWalletImportScene should render with loading props 1`] = ` Enter your private seed, private key, or active key to verify and restore the associated wallet - + - + + Private Key or Private Seed + + + - Private Key or Private Seed - + testID="undefined.textInput" + textAlignVertical="top" + /> - - - - - -  - + +  + + - + - - - + + - Next - + ] + } + > + Next + + diff --git a/src/__tests__/scenes/__snapshots__/FioAddressListScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/FioAddressListScene.test.tsx.snap index 81897f61bd9..a5e6f5da28d 100644 --- a/src/__tests__/scenes/__snapshots__/FioAddressListScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/FioAddressListScene.test.tsx.snap @@ -908,6 +908,7 @@ exports[`FioAddressList should render with loading props 1`] = ` > { const { absolute = false, + keyboardOpen = false, primary, secondary, secondary2, @@ -113,6 +118,7 @@ export const ButtonsView = React.memo((props: ButtonsViewProps) => { return ( @@ -156,10 +162,11 @@ export const ButtonsView = React.memo((props: ButtonsViewProps) => { /** @deprecated - Shouldn't use this export post-UI4 transition once all our layouts have been codified into button layout components. */ export const StyledButtonContainer = styled(View)<{ absolute?: boolean + keyboardOpen?: boolean layout: 'row' | 'column' | 'solo' parentType?: 'scene' | 'modal' }>(theme => props => { - const { absolute, layout, parentType } = props + const { absolute, keyboardOpen, layout, parentType } = props const marginSize = theme.rem(0.5) @@ -217,7 +224,11 @@ export const StyledButtonContainer = styled(View)<{ flexGrow: 1, flexShrink: 0, justifyContent: 'flex-end', - marginBottom: theme.rem(3), + // Clearance from the scene's bottom edge. An open keyboard becomes + // that edge and separates the button from the screen on its own, so + // this collapses the way `SceneWrapper` collapses the bottom + // safe-area inset, for the same reason: + marginBottom: keyboardOpen === true ? 0 : theme.rem(3), marginTop: theme.rem(1) } : {} diff --git a/src/components/scenes/CreateWalletImportScene.tsx b/src/components/scenes/CreateWalletImportScene.tsx index ebdb1c1f401..2535162e8bc 100644 --- a/src/components/scenes/CreateWalletImportScene.tsx +++ b/src/components/scenes/CreateWalletImportScene.tsx @@ -1,11 +1,11 @@ import type { JsonObject } from 'edge-core-js' import * as React from 'react' -import { Linking, Platform, View } from 'react-native' -import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view' +import { Linking, Platform, ScrollView, View } from 'react-native' import { sprintf } from 'sprintf-js' import { PLACEHOLDER_WALLET_ID } from '../../actions/CreateWalletActions' import ImportKeySvg from '../../assets/images/import-key-icon.svg' +import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { type ImportKeyOption, SPECIAL_CURRENCY_INFO @@ -244,137 +244,192 @@ const CreateWalletImportComponent: React.FC = props => { ) return ( - - - {/* We have to use the SceneHeaderUi4 component here because + + {({ isKeyboardOpen }) => ( + + {/* We have to use the SceneHeaderUi4 component here because the SceneContainer component does not implement the specific flex styles we need for this scene's container. These styles are a one-off case which has not been codified into our design hierarchy and made it completely into our abstraction (SceneContainer). */} - - - - - - - {lstrings.create_wallet_import_all_instructions} - - - {importOptsEntries.length > 0 ? ( - - {lstrings.create_wallet_import_options_title} - - ) : null} - {importOptsEntries.map(([pluginId, opts]) => ( - - {importOptsEntries.length > 1 ? ( - - - - {currencyConfig[pluginId].currencyInfo.displayName} - - - ) : null} - {[...opts].map(opt => { - const key = getOptionKey(pluginId, opt) - const item = optionValues.get(key) - if (item == null) return null - - const { value, error } = item - const { knowledgeBaseUri } = opt.displayDescription ?? {} - - const returnKeyType = - opt.inputType === 'number-pad' && Platform.OS === 'ios' - ? undefined - : 'done' - - return ( - - { - handleOptionChange(text, pluginId, opt) - }} - returnKeyType={returnKeyType} + {/* eslint-disable-next-line @typescript-eslint/no-deprecated */} + + + + + + + {lstrings.create_wallet_import_all_instructions} + + {/* FilledTextInput's multiline containers are flexGrow/flexShrink 1, + and a ScrollView lays its content out against the scroll viewport, so + the seed box would shrink to whatever room the keyboard leaves instead + of showing the whole phrase. This wrapper refuses to shrink, so the + field sizes to its text and the scene scrolls instead. + + numberOfLines is Android-only (iOS sizes the box to its text already), + and Android turns it into EditText.setLines, so the box holds this + many lines whether it is empty or full. A fixed count is the only + thing a caller can choose: omitting the prop falls back to + FilledTextInput's own default of 20, and 0 collapses the box to one + line that clips the phrase. Ten fits a 24-word seed on the phones we + support without leaving a large empty box on the ones we don't. */} + + + + {importOptsEntries.length > 0 ? ( + + {lstrings.create_wallet_import_options_title} + + ) : null} + {importOptsEntries.map(([pluginId, opts]) => ( + + {importOptsEntries.length > 1 ? ( + + - {knowledgeBaseUri != null ? ( - { - Linking.openURL(knowledgeBaseUri).catch( - (err: unknown) => { - showError(err) - } - ) - }} - > - - - ) : null} + + {currencyConfig[pluginId].currencyInfo.displayName} + - ) - })} + ) : null} + {[...opts].map(opt => { + const key = getOptionKey(pluginId, opt) + const item = optionValues.get(key) + if (item == null) return null + + const { value, error } = item + const { knowledgeBaseUri } = opt.displayDescription ?? {} + + const returnKeyType = + opt.inputType === 'number-pad' && Platform.OS === 'ios' + ? undefined + : 'done' + + return ( + + { + handleOptionChange(text, pluginId, opt) + }} + returnKeyType={returnKeyType} + /> + {knowledgeBaseUri != null ? ( + { + Linking.openURL(knowledgeBaseUri).catch( + (err: unknown) => { + showError(err) + } + ) + }} + > + + + ) : null} + + ) + })} + + ))} + {/* SceneButtons anchors itself to the bottom of a flex-sized + parent. This wrapper is content-sized, so the button simply follows + the content at its own margin instead of drifting with the amount of + free space: */} + + - ))} - - - + + + )} ) } const getStyles = cacheStyles((theme: Theme) => ({ container: { - flexShrink: 1, - margin: theme.rem(0.5) + flex: 1, + // No bottom margin: the scene's bottom edge IS the top of the keyboard + // while it is open, and a margin there is dead space the content can never + // scroll into: + marginTop: theme.rem(0.5), + marginHorizontal: theme.rem(0.5) + }, + scroll: { + // Take whatever room the header leaves, and give it back as the keyboard + // opens, so the content scrolls instead of running off the scene: + flex: 1 + }, + scrollContent: { + // Content-sized on purpose. Growing this to the viewport would hand the + // leftover room to whichever child can flex, so the scene's spacing would + // change with the length of the seed phrase: + flexGrow: 0 }, icon: { flexDirection: 'row', + alignItems: 'center', justifyContent: 'center', - marginVertical: theme.rem(2) + // A fixed 1 rem above and below, so the logo sits the same distance from + // the header whether the seed field holds nothing or a 24-word phrase: + marginVertical: theme.rem(1) + }, + seedInput: { + flexShrink: 0 }, optionsHeading: { fontSize: theme.rem(1), From 35dd1c7d512fa4d4a6b2d0ea2172a6433384367c Mon Sep 17 00:00:00 2001 From: Jonathan Tzeng Date: Mon, 31 Aug 2026 12:25:06 -0700 Subject: [PATCH 3/3] Fix lint errors in scene button components --- eslint.config.mjs | 3 +-- src/components/buttons/ButtonsView.tsx | 19 ++++++++++--------- src/components/buttons/SceneButtons.tsx | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index a818fa26926..8136a2d148b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -137,13 +137,12 @@ export default [ 'src/actions/WalletListActions.tsx', 'src/app.ts', - 'src/components/buttons/ButtonsView.tsx', + 'src/components/buttons/EdgeSwitch.tsx', 'src/components/buttons/IconButton.tsx', 'src/components/buttons/MinimalButton.tsx', 'src/components/buttons/ModalButtons.tsx', 'src/components/buttons/ReturnKeyTypeButton.tsx', - 'src/components/buttons/SceneButtons.tsx', 'src/components/cards/BalanceCard.tsx', 'src/components/cards/EarnOptionCard.tsx', diff --git a/src/components/buttons/ButtonsView.tsx b/src/components/buttons/ButtonsView.tsx index 4e95a1dab1d..36f3be55b75 100644 --- a/src/components/buttons/ButtonsView.tsx +++ b/src/components/buttons/ButtonsView.tsx @@ -80,7 +80,7 @@ export const ButtonsView = React.memo((props: ButtonsViewProps) => { type: EdgeButtonType, buttonProps?: ButtonInfo, index: number = 0 - ) => { + ): React.ReactElement | null => { if (buttonProps == null) return null const { label, onPress, disabled, spinner, testID } = buttonProps @@ -174,14 +174,15 @@ export const StyledButtonContainer = styled(View)<{ margin: marginSize } - const absoluteStyle: ViewStyle = absolute - ? { - position: 'absolute', - bottom: 0, - left: marginSize, - right: marginSize - } - : {} + const absoluteStyle: ViewStyle = + absolute === true + ? { + position: 'absolute', + bottom: 0, + left: marginSize, + right: marginSize + } + : {} /** @deprecated Instead of a soloStyle case here, create a separate `SoloButton` component */ const soloStyle: ViewStyle = diff --git a/src/components/buttons/SceneButtons.tsx b/src/components/buttons/SceneButtons.tsx index 45852a2d5d5..93830a9b983 100644 --- a/src/components/buttons/SceneButtons.tsx +++ b/src/components/buttons/SceneButtons.tsx @@ -10,6 +10,6 @@ interface Props extends Omit, 'layout'> {} /** For properly spacing out content behind floating absolute buttons */ export const SCENE_BUTTONS_MARGIN_REM = 7 -export const SceneButtons = (props: Props) => { +export const SceneButtons: React.FC = props => { return }