Skip to content

fix: prevent screenshot capture in nested modals#4134

Draft
shubhamkmr04 wants to merge 1 commit into
ZeusLN:screen-capture-protectionfrom
shubhamkmr04:refactor/screen-capture-modal-fixes
Draft

fix: prevent screenshot capture in nested modals#4134
shubhamkmr04 wants to merge 1 commit into
ZeusLN:screen-capture-protectionfrom
shubhamkmr04:refactor/screen-capture-modal-fixes

Conversation

@shubhamkmr04

@shubhamkmr04 shubhamkmr04 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Description

Based on #4118

This pull request is categorized as a:

  • New feature
  • Bug fix
  • Code refactor
  • Configuration change
  • Locales update
  • Quality assurance
  • Other

Checklist

  • I’ve run yarn run tsc and made sure my code compiles correctly
  • I’ve run yarn run lint and made sure my code didn’t contain any problematic patterns
  • I’ve run yarn run prettier and made sure my code is formatted correctly
  • I’ve run yarn run test and made sure all of the tests pass

Testing

If you modified or added a utility file, did you add new unit tests?

  • No, I’m a fool
  • Yes
  • N/A

I have tested this PR on the following platforms (please specify OS version and phone model/VM):

  • Android
  • iOS

I have tested this PR with the following types of nodes (please specify node version and API version where appropriate):

On-device

  • LDK Node
  • Embedded LND

Remote

  • LND (REST)
  • LND (Lightning Node Connect)
  • Core Lightning (CLNRest)
  • Nostr Wallet Connect
  • LndHub

Locales

  • I’ve added new locale text that requires translations
  • I’m aware that new translations should be made on the ZEUS Transfix page and not directly to this repo

Third Party Dependencies and Packages

  • Contributors will need to run yarn after this PR is merged in
  • 3rd party dependencies have been modified:
    • verify that package.json and yarn.lock have been properly updated
    • verify that dependencies are installed for both iOS and Android platforms

Other:

  • Changes were made that require an update to the README
  • Changes were made that require an update to onboarding

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a custom Portal and PortalProvider system to render modal overlays at the root of the view hierarchy, ensuring they are properly covered by native secure-canvas overlays. It replaces standard React Native Modal components with ModalBox and a new CenteredModal component across several pickers and views. The review feedback highlights a few critical improvements: dynamically retrieving screen height in HopPicker to handle orientation changes, wrapping CenteredModal in a <Portal> and ensuring its children are wrapped in a <View> to prevent runtime crashes with TouchableWithoutFeedback, and adding a dependency array to the useEffect hook in Portal.tsx to avoid redundant state updates.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread components/HopPicker.tsx

const DEFAULT_TITLE = localeString('components.HopPicker.defaultTitle');
const MAX_NUMBER_ROUTE_HINTS_LND = 20;
const { height: SCREEN_HEIGHT } = Dimensions.get('window');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Caching the screen height at the module level using Dimensions.get('window') is a React Native anti-pattern. If the device orientation changes, or if the app is run in split-screen/multi-window mode (common on tablets and foldables), SCREEN_HEIGHT will not be updated, leading to incorrect modal heights and layout issues.

Instead, you should retrieve the window dimensions dynamically inside the render method.

Comment thread components/HopPicker.tsx
Comment on lines 223 to 224

return (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Retrieve the screen height dynamically inside the render method to ensure it always reflects the current device orientation and window dimensions.

        const SCREEN_HEIGHT = Dimensions.get('window').height;

        return (

Comment on lines +1 to +44
import * as React from 'react';
import { TouchableWithoutFeedback, View } from 'react-native';

import ModalBox from '../ModalBox';

interface CenteredModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}

const CenteredModal: React.FC<CenteredModalProps> = ({
isOpen,
onClose,
children
}) => (
<ModalBox
isOpen={isOpen}
onClosed={onClose}
entry="center"
backdrop
backdropPressToClose
backButtonClose
swipeToClose={false}
backdropOpacity={0.5}
style={{ backgroundColor: 'transparent' }}
>
<TouchableWithoutFeedback onPress={onClose}>
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}}
>
<TouchableWithoutFeedback onPress={() => {}}>
{children}
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</ModalBox>
);

export default CenteredModal;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

To ensure that CenteredModal always renders at the root of the view hierarchy and avoids any nested modal or z-index issues (which is the primary goal of this PR), it should be wrapped in the newly introduced <Portal> component, just like HopPicker and UTXOPicker.

Additionally, TouchableWithoutFeedback in React Native expects exactly one child element. If children passed to CenteredModal is a fragment, an array of elements, or null/undefined, the app will crash at runtime with React.Children.only expected to receive a single React element child. Wrapping {children} in a <View> inside the inner TouchableWithoutFeedback prevents this crash.

import * as React from 'react';
import { TouchableWithoutFeedback, View } from 'react-native';

import ModalBox from '../ModalBox';
import { Portal } from '../Portal';

interface CenteredModalProps {
    isOpen: boolean;
    onClose: () => void;
    children: React.ReactNode;
}

const CenteredModal: React.FC<CenteredModalProps> = ({
    isOpen,
    onClose,
    children
}) => (
    <Portal>
        <ModalBox
            isOpen={isOpen}
            onClosed={onClose}
            entry="center"
            backdrop
            backdropPressToClose
            backButtonClose
            swipeToClose={false}
            backdropOpacity={0.5}
            style={{ backgroundColor: 'transparent' }}
        >
            <TouchableWithoutFeedback onPress={onClose}>
                <View
                    style={{
                        flex: 1,
                        justifyContent: 'center',
                        alignItems: 'center'
                    }}
                >
                    <TouchableWithoutFeedback onPress={() => {}}>
                        <View>{children}</View>
                    </TouchableWithoutFeedback>
                </View>
            </TouchableWithoutFeedback>
        </ModalBox>
    </Portal>
);

export default CenteredModal;

Comment thread components/Portal.tsx
Comment on lines +63 to +65
React.useEffect(() => {
ctx?.setNode(key, children);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The useEffect hook that registers/updates the portal node has no dependency array, meaning it runs on every single render of the Portal component. If the parent component re-renders frequently (e.g., during animations, text input, or state updates), this will trigger setNode on every render, causing unnecessary state updates in PortalProvider and redundant re-renders of the portal container.

Adding [ctx, key, children] as the dependency array ensures that the portal registration is only updated when the context, key, or children actually change.

Suggested change
React.useEffect(() => {
ctx?.setNode(key, children);
});
React.useEffect(() => {
ctx?.setNode(key, children);
}, [ctx, key, children]);
References
  1. Avoid redundant state updates. Do not reset a state variable if it has already been set to the desired value by a previous action.

@shubhamkmr04 shubhamkmr04 force-pushed the refactor/screen-capture-modal-fixes branch from c87eb64 to 767965e Compare June 3, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant