Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix scroll on native when going back to the login page #25590

Merged
merged 20 commits into from
Aug 24, 2023
Merged
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
@@ -1,43 +1,46 @@
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import _ from 'underscore';
import Str from 'expensify-common/lib/str';
import {parsePhoneNumber} from 'awesome-phonenumber';
import styles from '../../styles/styles';
import Text from '../../components/Text';
import * as Session from '../../libs/actions/Session';
import ONYXKEYS from '../../ONYXKEYS';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions';
import compose from '../../libs/compose';
import canFocusInputOnScreenFocus from '../../libs/canFocusInputOnScreenFocus';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
import TextInput from '../../components/TextInput';
import * as ValidationUtils from '../../libs/ValidationUtils';
import * as LoginUtils from '../../libs/LoginUtils';
import withToggleVisibilityView, {toggleVisibilityViewPropTypes} from '../../components/withToggleVisibilityView';
import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton';
import {withNetwork} from '../../components/OnyxProvider';
import networkPropTypes from '../../components/networkPropTypes';
import * as ErrorUtils from '../../libs/ErrorUtils';
import DotIndicatorMessage from '../../components/DotIndicatorMessage';
import * as CloseAccount from '../../libs/actions/CloseAccount';
import CONST from '../../CONST';
import CONFIG from '../../CONFIG';
import AppleSignIn from '../../components/SignInButtons/AppleSignIn';
import GoogleSignIn from '../../components/SignInButtons/GoogleSignIn';
import isInputAutoFilled from '../../libs/isInputAutoFilled';
import * as PolicyUtils from '../../libs/PolicyUtils';
import Log from '../../libs/Log';
import withNavigationFocus, {withNavigationFocusPropTypes} from '../../components/withNavigationFocus';
import usePrevious from '../../hooks/usePrevious';
import * as MemoryOnlyKeys from '../../libs/actions/MemoryOnlyKeys/MemoryOnlyKeys';
import styles from '../../../styles/styles';
import Text from '../../../components/Text';
import * as Session from '../../../libs/actions/Session';
import ONYXKEYS from '../../../ONYXKEYS';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
import compose from '../../../libs/compose';
import canFocusInputOnScreenFocus from '../../../libs/canFocusInputOnScreenFocus';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
import TextInput from '../../../components/TextInput';
import * as ValidationUtils from '../../../libs/ValidationUtils';
import * as LoginUtils from '../../../libs/LoginUtils';
import withToggleVisibilityView, {toggleVisibilityViewPropTypes} from '../../../components/withToggleVisibilityView';
import FormAlertWithSubmitButton from '../../../components/FormAlertWithSubmitButton';
import {withNetwork} from '../../../components/OnyxProvider';
import networkPropTypes from '../../../components/networkPropTypes';
import * as ErrorUtils from '../../../libs/ErrorUtils';
import DotIndicatorMessage from '../../../components/DotIndicatorMessage';
import * as CloseAccount from '../../../libs/actions/CloseAccount';
import CONST from '../../../CONST';
import CONFIG from '../../../CONFIG';
import AppleSignIn from '../../../components/SignInButtons/AppleSignIn';
import GoogleSignIn from '../../../components/SignInButtons/GoogleSignIn';
import isInputAutoFilled from '../../../libs/isInputAutoFilled';
import * as PolicyUtils from '../../../libs/PolicyUtils';
import Log from '../../../libs/Log';
import withNavigationFocus, {withNavigationFocusPropTypes} from '../../../components/withNavigationFocus';
import usePrevious from '../../../hooks/usePrevious';
import * as MemoryOnlyKeys from '../../../libs/actions/MemoryOnlyKeys/MemoryOnlyKeys';

const propTypes = {
/** Should we dismiss the keyboard when transitioning away from the page? */
blurOnSubmit: PropTypes.bool,

/** A reference so we can expose if the form input is focused */
innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),

/* Onyx Props */

/** The details about the account that the user is signing in with */
Expand Down Expand Up @@ -73,6 +76,7 @@ const defaultProps = {
account: {},
closeAccount: {},
blurOnSubmit: false,
innerRef: () => {},
};

function LoginForm(props) {
Expand Down Expand Up @@ -177,6 +181,12 @@ function LoginForm(props) {
input.current.focus();
}, [props.blurOnSubmit, props.isVisible, prevIsVisible]);

useImperativeHandle(props.innerRef, () => ({
isInputFocused() {
return input.current && input.current.isFocused();
},
}));

const formErrorText = useMemo(() => (formError ? translate(formError) : ''), [formError, translate]);
const serverErrorText = useMemo(() => ErrorUtils.getLatestErrorMessage(props.account), [props.account]);
const hasError = !_.isEmpty(serverErrorText);
Expand Down Expand Up @@ -272,4 +282,12 @@ export default compose(
withLocalize,
withToggleVisibilityView,
withNetwork(),
)(LoginForm);
)(
forwardRef((props, ref) => (
<LoginForm
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
innerRef={ref}
/>
)),
);
26 changes: 26 additions & 0 deletions src/pages/signin/LoginForm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import BaseLoginForm from './BaseLoginForm';

const propTypes = {
/** Function used to scroll to the top of the page */
scrollPageToTop: PropTypes.func,
};
const defaultProps = {
scrollPageToTop: undefined,
};

function LoginForm(props) {
return (
<BaseLoginForm
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
);
}

LoginForm.displayName = 'LoginForm';
LoginForm.propTypes = propTypes;
LoginForm.defaultProps = defaultProps;

export default LoginForm;
48 changes: 48 additions & 0 deletions src/pages/signin/LoginForm/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, {useEffect, useRef} from 'react';
import PropTypes from 'prop-types';
import BaseLoginForm from './BaseLoginForm';
import AppStateMonitor from '../../../libs/AppStateMonitor';

const propTypes = {
/** Function used to scroll to the top of the page */
scrollPageToTop: PropTypes.func,
};
const defaultProps = {
scrollPageToTop: undefined,
};

function LoginForm(props) {
Copy link
Contributor

Choose a reason for hiding this comment

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

NAB

Suggested change
function LoginForm(props) {
function LoginForm({scrollPageToTop}) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The props are sent to the BaseLoginForm here: https://github.com/joh42/expensify-app/blob/2661fb84b0e3c4921aafa9fc3fe3e794be602d88/src/pages/signin/LoginForm/index.native.js#L38

But {scrollPageToTop, ...props} could work if that's preferred

const loginFormRef = useRef();
const {scrollPageToTop} = props;

useEffect(() => {
if (!scrollPageToTop) {
return;
}

const unsubscribeToBecameActiveListener = AppStateMonitor.addBecameActiveListener(() => {
const isInputFocused = loginFormRef.current && loginFormRef.current.isInputFocused();
if (!isInputFocused) {
return;
}

scrollPageToTop();
});

return unsubscribeToBecameActiveListener;
}, [scrollPageToTop]);

return (
<BaseLoginForm
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={(ref) => (loginFormRef.current = ref)}
/>
);
}

LoginForm.displayName = 'LoginForm';
LoginForm.propTypes = propTypes;
LoginForm.defaultProps = defaultProps;

export default LoginForm;
5 changes: 4 additions & 1 deletion src/pages/signin/SignInPage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect} from 'react';
import React, {useEffect, useRef} from 'react';
import PropTypes from 'prop-types';
import _ from 'underscore';
import {withOnyx} from 'react-native-onyx';
Expand Down Expand Up @@ -95,6 +95,7 @@ function SignInPage({credentials, account, isInModal, demoInfo}) {
const {isSmallScreenWidth} = useWindowDimensions();
const shouldShowSmallScreen = isSmallScreenWidth || isInModal;
const safeAreaInsets = useSafeAreaInsets();
const signInPageLayoutRef = useRef();

useEffect(() => Performance.measureTTI(), []);
useEffect(() => {
Expand Down Expand Up @@ -161,6 +162,7 @@ function SignInPage({credentials, account, isInModal, demoInfo}) {
welcomeText={welcomeText}
shouldShowWelcomeHeader={shouldShowWelcomeHeader || !isSmallScreenWidth || !isInModal}
shouldShowWelcomeText={shouldShowWelcomeText}
ref={signInPageLayoutRef}
isInModal={isInModal}
customHeadline={customHeadline}
>
Expand All @@ -169,6 +171,7 @@ function SignInPage({credentials, account, isInModal, demoInfo}) {
<LoginForm
isVisible={shouldShowLoginForm}
blurOnSubmit={account.validated === false}
scrollPageToTop={signInPageLayoutRef.current && signInPageLayoutRef.current.scrollPageToTop}
/>
{shouldShowValidateCodeForm && <ValidateCodeForm />}
{shouldShowUnlinkLoginForm && <UnlinkLoginForm />}
Expand Down
24 changes: 22 additions & 2 deletions src/pages/signin/SignInPageLayout/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useRef, useEffect} from 'react';
import React, {forwardRef, useRef, useEffect, useImperativeHandle} from 'react';
import {View, ScrollView} from 'react-native';
import {withSafeAreaInsets} from 'react-native-safe-area-context';
import PropTypes from 'prop-types';
Expand Down Expand Up @@ -35,6 +35,9 @@ const propTypes = {
/** Whether to show welcome header on a particular page */
shouldShowWelcomeHeader: PropTypes.bool.isRequired,

/** A reference so we can expose scrollPageToTop */
innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),

/** Whether or not the sign in page is being rendered in the RHP modal */
isInModal: PropTypes.bool.isRequired,

Expand All @@ -46,6 +49,7 @@ const propTypes = {
};

const defaultProps = {
innerRef: () => {},
customHeadline: '',
};

Expand All @@ -71,6 +75,10 @@ function SignInPageLayout(props) {
scrollViewRef.current.scrollTo({y: 0, animated});
};

useImperativeHandle(props.innerRef, () => ({
scrollPageToTop,
}));

useEffect(() => {
if (prevPreferredLocale !== props.preferredLocale) {
return;
Expand Down Expand Up @@ -160,4 +168,16 @@ SignInPageLayout.propTypes = propTypes;
SignInPageLayout.displayName = 'SignInPageLayout';
SignInPageLayout.defaultProps = defaultProps;

export default compose(withWindowDimensions, withSafeAreaInsets, withLocalize)(SignInPageLayout);
export default compose(
withWindowDimensions,
withSafeAreaInsets,
withLocalize,
)(
forwardRef((props, ref) => (
<SignInPageLayout
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
innerRef={ref}
/>
)),
);
Loading