Skip to content

Conversation

@brianacnguyen
Copy link
Contributor

@brianacnguyen brianacnguyen commented Jan 8, 2026

Description

This PR updates the header and footer components in the import token/NFT flows to align with the design system. The changes include:

  1. Replaced legacy navbar functions with the new HeaderCenter component:

    • getImportTokenNavbarOptionsgetHeaderCenterNavbarOptions
    • getWebviewNavbargetHeaderCenterNavbarOptions (removed from Navbar)
  2. Updated bottom action buttons layout:

    • Moved action buttons outside of KeyboardAwareScrollView in ActionView so they remain fixed at the bottom of the screen
    • Updated spacing/padding for better consistency across iOS and Android
  3. Replaced Alert dialogs with Toast notifications in AddCustomCollectible for ownership verification errors

  4. Updated bottom sheets to use HeaderCenter and BottomSheetFooter components instead of SheetHeader and SheetActionView

  5. Fixed button text casing for NFT import flow (CANCEL → Cancel, IMPORT → Import)

Changelog

CHANGELOG entry: Updated headers and footers in import token and NFT flows to use design system components

Related issues

Fixes: https://consensyssoftware.atlassian.net/jira/software/c/projects/MDP/boards/2972?assignee=62afb43d33a882e2be47c36f&quickFilter=3325&selectedIssue=MDP-269

Manual testing steps

Feature: Import Token/NFT Header and Footer Updates

  Scenario: User imports a custom token
    Given the user is on the wallet home screen
    When user taps "Import tokens" from the token list
    Then the header displays centered title with back button
    And the footer buttons remain fixed at the bottom

  Scenario: User imports a custom NFT
    Given the user is on the NFT tab
    When user taps "Import NFT"
    Then the header displays centered title with back button
    And the Cancel/Import buttons show proper casing (not all caps)

  Scenario: User attempts to import an NFT they don't own
    Given the user is on the import NFT screen
    When user enters an NFT address/ID they don't own and taps Import
    Then a toast notification appears with the ownership error (not an Alert dialog)

  Scenario: User opens SimpleWebview
    Given the user is in the app
    When user navigates to a simple webview (e.g., terms of service link)
    Then the header displays with share icon on the right

Screenshots/Recordings

Before

After

Import Token
https://github.com/user-attachments/assets/95ed563b-b94f-4c4b-a437-2fc174a758cf

Import NFT
https://github.com/user-attachments/assets/e08d9e0f-6133-4af5-8289-f9d881439194

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Aligns import asset flows with design-system components and stabilizes action CTAs across screens.

  • Replace legacy nav headers with getHeaderCenterNavbarOptions in AddAsset, ConfirmAddAsset, SimpleWebview, and related bottom sheets; add share icon via header end button in SimpleWebview; remove getWebviewNavbar
  • Anchor action buttons by moving ActionView footer outside KeyboardAwareScrollView and adjust platform-specific padding
  • Swap Alert dialogs for Toasts in AddCustomCollectible ownership checks; update tests to use ToastContext
  • Standardize spacing and footer layouts; use BottomSheetFooter and HeaderCenter in sheets; fix NFT import button casing and update locale strings
  • Update snapshots/tests accordingly

Written by Cursor Bugbot for commit dbf963c. This will update automatically on new commits. Configure here.

@brianacnguyen brianacnguyen requested a review from a team as a code owner January 8, 2026 00:03
@brianacnguyen brianacnguyen marked this pull request as draft January 8, 2026 00:03
@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbot metamaskbot added the team-design-system All issues relating to design system in Mobile label Jan 8, 2026
@github-actions github-actions bot added the size-M label Jan 8, 2026
@brianacnguyen brianacnguyen changed the title Header/importtokennft chore: Align headers for Import Assets flow Jan 8, 2026
@brianacnguyen brianacnguyen self-assigned this Jan 8, 2026
@brianacnguyen brianacnguyen added the No QA Needed Apply this label when your PR does not need any QA effort. label Jan 8, 2026
@brianacnguyen brianacnguyen marked this pull request as ready for review January 8, 2026 00:26
@github-actions github-actions bot added size-L and removed size-M labels Jan 8, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeAssets, SmokeConfirmationsRedesigned, SmokeCore, SmokeWalletUX
  • Risk Level: medium
  • AI Confidence: 80%
click to see 🤖 AI reasoning details

This PR is a UI/UX refactoring that affects multiple areas of the app:

  1. ActionView Component - A widely-used component that provides Cancel/Confirm button layouts. The change moves buttons outside the scroll view and adjusts padding. This component is used in:

    • Legacy confirmations (Approval, Approve, TransactionReview, ApproveTransactionReview, SignatureRequest)
    • NFT/Collectible adding
    • Bookmark adding
    • QR Hardware signing
    • Reveal private credentials
    • Onboarding flows
  2. Navigation Headers - Replaces getImportTokenNavbarOptions and getWebviewNavbar with new getHeaderCenterNavbarOptions:

    • AddAsset screen (token/NFT import)
    • ConfirmAddAsset screen
    • SimpleWebview
    • NetworkListBottomSheet
  3. NFT-specific changes:

    • AddCustomCollectible now uses Toast instead of Alert for error messages
    • ShowDisplayNFTMediaSheet uses new HeaderCenter and BottomSheetFooter components
  4. Locale changes - Button text case changes for collectibles ("CANCEL" → "Cancel", "IMPORT" → "Import")

Selected tags rationale:

  • SmokeAssets: NFT/collectible adding, token import, asset display changes
  • SmokeConfirmationsRedesigned: ActionView changes affect legacy confirmation flows
  • SmokeCore: Core UI component (ActionView) changes, navigation changes
  • SmokeWalletUX: UI/UX changes, button styling, toast notifications, header changes

The risk is medium because:

  • Changes are primarily visual/layout (not business logic)
  • ActionView is widely used but the change is structural (button positioning)
  • No changes to core wallet functionality or security-sensitive code
  • Snapshot tests have been updated appropriately

View GitHub Actions results

description: strings('collectible.ownership_verification_error'),
},
hasNoTimeout: false,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential crash when calling navigation.goBack() on optional navigation prop

The navigation prop is defined as optional (line 112: navigation?: any), but navigation.goBack() is called directly without null checking at lines 295 and 299. If the component is rendered without the navigation prop, this will cause a runtime crash when addNft() completes successfully or when cancelAddCollectible() is called.

Other components in the codebase correctly use optional chaining for this pattern (e.g., app/components/UI/Card/routes/index.tsx:86 uses navigation?.goBack()).

🔬 Verification Test

Code analysis:

// Line 112 - navigation is optional
interface AddCustomCollectibleProps {
  navigation?: any;  // Optional
  ...
}

// Line 295 - No null check before calling goBack()
const addNft = async (): Promise<void> => {
  ...
  setLoading(false);
  navigation.goBack();  // Will crash if navigation is undefined
};

// Line 299 - No null check before calling goBack()
const cancelAddCollectible = (): void => {
  navigation.goBack();  // Will crash if navigation is undefined
};

Test evidence:
The test file at line 669-679 (Props Variations section) explicitly tests rendering without the navigation prop:

it('renders without navigation prop', () => {
  const { getByTestId } = renderWithProvider(
    <AddCustomCollectible
      setOpenNetworkSelector={jest.fn()}
      networkId={'0x1'}
      selectedNetwork={'Ethereum Mainnet'}
      networkClientId={'mainnet'}
    />,
    { state: initialRootState },
  );
  expect(getByTestId('import-nft-screen')).toBeTruthy();
});

This test passes because it only renders the component. If this test attempted to trigger addNft or cancelAddCollectible, it would crash.

Fix: Replace navigation.goBack() with navigation?.goBack() at both locations.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

description: strings('collectible.ownership_verification_error'),
},
hasNoTimeout: false,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential crash when calling navigation.goBack() on optional navigation prop

The navigation prop is defined as optional (line 112: navigation?: any), but navigation.goBack() is called directly without null checking at lines 295 and 299. If the component is rendered without the navigation prop, this will cause a runtime crash when addNft() completes successfully or when cancelAddCollectible() is called.

Other components in the codebase correctly use optional chaining for this pattern (e.g., app/components/UI/Card/routes/index.tsx:86 uses navigation?.goBack()).

The test file explicitly tests rendering without the navigation prop in the "Props Variations" section (line 668-679), confirming this is a valid use case. If that test attempted to trigger addNft or cancelAddCollectible, it would crash.

Fix: Replace navigation.goBack() with navigation?.goBack() at both locations.

Fix in Cursor Fix in Web

);

if (!isOwner)
Alert.alert(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChainId mismatch - validation uses global state chainId instead of selected network's chainId

The validateCustomCollectibleAddress function uses chainId from global state (retrieved via useSelector(selectChainId) on line 150), but the component receives a networkId prop that represents the user-selected network's chain ID. When a user selects a different network via the network selector UI, the validation will still occur against the globally-selected network, not the one the user chose.

This causes:

  1. Smart contract address validation to query the wrong blockchain
  2. NFTs may fail to validate even when they exist on the selected network
  3. Analytics tracking (getAnalyticsParams at line 169) reports the wrong chain ID

In AddAsset.tsx:279, the networkId prop is correctly set to the selected network's chain ID, but AddCustomCollectible ignores this prop for validation and uses the global chainId instead. Notably, the Avatar component at line 344 correctly uses networkId, making this inconsistency more evident.

Scenario: User's wallet is connected to Polygon, but they select Ethereum Mainnet in the NFT import network selector. The smart contract validation will query Polygon (global state) instead of Ethereum (selected network), causing incorrect validation results.

Fix: Replace chainId with networkId in the isSmartContractAddress call and getAnalyticsParams function.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

description: strings('collectible.ownership_verification_error'),
},
hasNoTimeout: false,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent networkClientId fallback logic - validation uses fallback but addNft/ownership check do not

High Severity

The validateCustomCollectibleAddress function at line 181 uses a fallback pattern for the network client ID:

const clientId = networkClientId || selectedNetworkClientId;

However, validateCollectibleOwnership (line 228) and addNft (line 281) use networkClientId directly without the fallback:

// Line 228 in validateCollectibleOwnership:
const isOwner = await NftController.isNftOwner(
  selectedAddress,
  address,
  tokenId,
  networkClientId,  // No fallback!
);

// Line 281 in addNft:
await NftController.addNft(address, tokenId, networkClientId);  // No fallback!

This inconsistency can cause:

  1. Address validation passes using selectedNetworkClientId as fallback when networkClientId is null
  2. Ownership validation and NFT addition then fail or behave unexpectedly because they receive null instead of the fallback value
  3. The NFT could be added to the wrong network or the operation could fail silently

The networkClientId prop is typed as string | null (line 119), so null values are explicitly allowed.

Fix in Cursor Fix in Web

description: strings('collectible.ownership_verification_error'),
},
hasNoTimeout: false,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing try-catch in addNft causes UI to get stuck in loading state on error

High Severity

The addNft function sets setLoading(true) at the start but has no try-catch block around the operations that can throw. If NftController.addNft (line 281) or isSmartContractAddress (called via validateCustomCollectible) throws an error (e.g., network failure), the function will exit without:

  1. Calling setLoading(false) - leaving the UI stuck in a loading state with a disabled button
  2. Calling endTrace({ name: TraceName.ImportNfts }) - leaving an orphaned trace
  3. Showing any error feedback to the user

Contrast this with validateCollectibleOwnership (lines 219-262) which properly wraps its async operations in try-catch and shows a toast on error.

const addNft = async (): Promise<void> => {
  setLoading(true);
  // ... validation ...
  
  trace({ name: TraceName.ImportNfts });
  await NftController.addNft(address, tokenId, networkClientId);  // Can throw!
  endTrace({ name: TraceName.ImportNfts });  // Never reached on error
  
  // ...
  setLoading(false);  // Never reached on error - UI stuck!
  navigation.goBack();
};

The isSmartContractAddress function in app/util/transactions/index.js also lacks error handling and will throw on network errors, propagating up through validateCustomCollectibleAddressvalidateCustomCollectibleaddNft.

Fix in Cursor Fix in Web

@sonarqubecloud
Copy link

sonarqubecloud bot commented Jan 9, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
74.1% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

No QA Needed Apply this label when your PR does not need any QA effort. size-L team-design-system All issues relating to design system in Mobile

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants