diff --git a/src/components/SignInButtons/AppleSignIn/index.website.tsx b/src/components/SignInButtons/AppleSignIn/index.tsx similarity index 100% rename from src/components/SignInButtons/AppleSignIn/index.website.tsx rename to src/components/SignInButtons/AppleSignIn/index.tsx diff --git a/src/components/SignInButtons/GoogleSignIn/index.website.tsx b/src/components/SignInButtons/GoogleSignIn/index.tsx similarity index 100% rename from src/components/SignInButtons/GoogleSignIn/index.website.tsx rename to src/components/SignInButtons/GoogleSignIn/index.tsx diff --git a/src/components/TextLink.tsx b/src/components/TextLink.tsx index c8cd39b05fcc..affd6b046dee 100644 --- a/src/components/TextLink.tsx +++ b/src/components/TextLink.tsx @@ -79,4 +79,6 @@ function TextLink({href, onPress, children, style, onMouseDown = (event) => even TextLink.displayName = 'TextLink'; +export type {LinkProps, PressProps}; + export default forwardRef(TextLink); diff --git a/src/components/withToggleVisibilityView.tsx b/src/components/withToggleVisibilityView.tsx index 86513f1bd0dc..17fda7fd5e30 100644 --- a/src/components/withToggleVisibilityView.tsx +++ b/src/components/withToggleVisibilityView.tsx @@ -5,12 +5,12 @@ import type {SetOptional} from 'type-fest'; import useThemeStyles from '@hooks/useThemeStyles'; import getComponentDisplayName from '@libs/getComponentDisplayName'; -type ToggleVisibilityViewProps = { +type WithToggleVisibilityViewProps = { /** Whether the content is visible. */ - isVisible: boolean; + isVisible?: boolean; }; -export default function withToggleVisibilityView( +export default function withToggleVisibilityView( WrappedComponent: ComponentType>, ): (props: TProps & RefAttributes) => ReactElement | null { function WithToggleVisibilityView({isVisible = false, ...rest}: SetOptional, ref: ForwardedRef) { @@ -30,3 +30,5 @@ export default function withToggleVisibilityView(elements: readon /** * Returns the user device's preferred language. */ -function getDevicePreferredLocale(): string { +function getDevicePreferredLocale(): Locale { return RNLocalize.findBestAvailableLanguage([CONST.LOCALES.EN, CONST.LOCALES.ES])?.languageTag ?? CONST.LOCALES.DEFAULT; } diff --git a/src/libs/Performance.tsx b/src/libs/Performance.tsx index 90a13b84d7f7..5ac064e75727 100644 --- a/src/libs/Performance.tsx +++ b/src/libs/Performance.tsx @@ -27,7 +27,7 @@ type PrintPerformanceMetrics = () => void; type MarkStart = (name: string, detail?: Record) => PerformanceMark | void; type MarkEnd = (name: string, detail?: Record) => PerformanceMark | void; type MeasureFailSafe = (measureName: string, startOrMeasureOptions: string, endMark?: string) => void; -type MeasureTTI = (endMark: string) => void; +type MeasureTTI = (endMark?: string) => void; type TraceRender = (id: string, phase: Phase, actualDuration: number, baseDuration: number, startTime: number, commitTime: number, interactions: Set) => PerformanceMeasure | void; type WithRenderTrace = ({id}: WrappedComponentConfig) => WithRenderTraceHOC | BlankHOC; type SubscribeToMeasurements = (callback: PerformanceEntriesCallback) => void; @@ -104,7 +104,7 @@ if (Metrics.canCapturePerformanceMetrics()) { /** * Measures the TTI time. To be called when the app is considered to be interactive. */ - Performance.measureTTI = (endMark: string) => { + Performance.measureTTI = (endMark?: string) => { // Make sure TTI is captured when the app is really usable InteractionManager.runAfterInteractions(() => { requestAnimationFrame(() => { diff --git a/src/pages/signin/AppleSignInDesktopPage/index.website.tsx b/src/pages/signin/AppleSignInDesktopPage/index.website.tsx index e23280a83132..867dfddc443d 100644 --- a/src/pages/signin/AppleSignInDesktopPage/index.website.tsx +++ b/src/pages/signin/AppleSignInDesktopPage/index.website.tsx @@ -3,12 +3,7 @@ import ThirdPartySignInPage from '@pages/signin/ThirdPartySignInPage'; import CONST from '@src/CONST'; function AppleSignInDesktopPage() { - return ( - - ); + return ; } export default AppleSignInDesktopPage; diff --git a/src/pages/signin/ChooseSSOOrMagicCode.js b/src/pages/signin/ChooseSSOOrMagicCode.tsx similarity index 67% rename from src/pages/signin/ChooseSSOOrMagicCode.js rename to src/pages/signin/ChooseSSOOrMagicCode.tsx index 13d20e689128..d3140da278e8 100644 --- a/src/pages/signin/ChooseSSOOrMagicCode.js +++ b/src/pages/signin/ChooseSSOOrMagicCode.tsx @@ -1,8 +1,7 @@ -import PropTypes from 'prop-types'; import React, {useEffect} from 'react'; import {Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; import Text from '@components/Text'; @@ -17,43 +16,25 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Account, Credentials} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ChangeExpensifyLoginLink from './ChangeExpensifyLoginLink'; import Terms from './Terms'; -const propTypes = { - /* Onyx Props */ - +type ChooseSSOOrMagicCodeOnyxProps = { /** The credentials of the logged in person */ - credentials: PropTypes.shape({ - /** The email/phone the user logged in with */ - login: PropTypes.string, - }), + credentials: OnyxEntry; /** The details about the account that the user is signing in with */ - account: PropTypes.shape({ - /** Whether or not a sign on form is loading (being submitted) */ - isLoading: PropTypes.bool, - - /** Form that is being loaded */ - loadingForm: PropTypes.oneOf(_.values(CONST.FORMS)), - - /** Whether this account has 2FA enabled or not */ - requiresTwoFactorAuth: PropTypes.bool, - - /** Server-side errors in the submitted authentication code */ - errors: PropTypes.objectOf(PropTypes.string), - }), - - /** Function that returns whether the user is using SAML or magic codes to log in */ - setIsUsingMagicCode: PropTypes.func.isRequired, + account: OnyxEntry; }; -const defaultProps = { - credentials: {}, - account: {}, +type ChooseSSOOrMagicCodeProps = ChooseSSOOrMagicCodeOnyxProps & { + /** Function that returns whether the user is using SAML or magic codes to log in */ + setIsUsingMagicCode: (value: boolean) => void; }; -function ChooseSSOOrMagicCode({credentials, account, setIsUsingMagicCode}) { +function ChooseSSOOrMagicCode({credentials, account, setIsUsingMagicCode}: ChooseSSOOrMagicCodeProps) { const styles = useThemeStyles(); const {isKeyboardShown} = useKeyboardState(); const {translate} = useLocalize(); @@ -77,7 +58,7 @@ function ChooseSSOOrMagicCode({credentials, account, setIsUsingMagicCode}) { success style={[styles.mv3]} text={translate('samlSignIn.useSingleSignOn')} - isLoading={account.isLoading} + isLoading={account?.isLoading} onPress={() => { Navigation.navigate(ROUTES.SAML_SIGN_IN); }} @@ -93,14 +74,17 @@ function ChooseSSOOrMagicCode({credentials, account, setIsUsingMagicCode}) { isDisabled={isOffline} style={[styles.mv3]} text={translate('samlSignIn.useMagicCode')} - isLoading={account.isLoading && account.loadingForm === (account.requiresTwoFactorAuth ? CONST.FORMS.VALIDATE_TFA_CODE_FORM : CONST.FORMS.VALIDATE_CODE_FORM)} + isLoading={account?.isLoading && account?.loadingForm === (account?.requiresTwoFactorAuth ? CONST.FORMS.VALIDATE_TFA_CODE_FORM : CONST.FORMS.VALIDATE_CODE_FORM)} onPress={() => { - Session.resendValidateCode(credentials.login); + Session.resendValidateCode(credentials?.login); setIsUsingMagicCode(true); }} /> - {Boolean(account) && !_.isEmpty(account.errors) && } - Session.clearSignInData()} /> + {!!account && !isEmptyObject(account.errors) && } + Session.clearSignInData()} + /> @@ -109,11 +93,9 @@ function ChooseSSOOrMagicCode({credentials, account, setIsUsingMagicCode}) { ); } -ChooseSSOOrMagicCode.propTypes = propTypes; -ChooseSSOOrMagicCode.defaultProps = defaultProps; ChooseSSOOrMagicCode.displayName = 'ChooseSSOOrMagicCode'; -export default withOnyx({ +export default withOnyx({ credentials: {key: ONYXKEYS.CREDENTIALS}, account: {key: ONYXKEYS.ACCOUNT}, })(ChooseSSOOrMagicCode); diff --git a/src/pages/signin/EmailDeliveryFailurePage.js b/src/pages/signin/EmailDeliveryFailurePage.tsx similarity index 81% rename from src/pages/signin/EmailDeliveryFailurePage.js rename to src/pages/signin/EmailDeliveryFailurePage.tsx index 9996374cc6cd..e2e12dbd0c1c 100644 --- a/src/pages/signin/EmailDeliveryFailurePage.js +++ b/src/pages/signin/EmailDeliveryFailurePage.tsx @@ -1,8 +1,8 @@ import Str from 'expensify-common/lib/str'; -import PropTypes from 'prop-types'; -import React, {useEffect} from 'react'; +import React, {useEffect, useMemo} from 'react'; import {Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; @@ -12,26 +12,25 @@ import useThemeStyles from '@hooks/useThemeStyles'; import redirectToSignIn from '@userActions/SignInRedirect'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Credentials} from '@src/types/onyx'; -const propTypes = { - /* Onyx Props */ - +type EmailDeliveryFailurePageOnyxProps = { /** The credentials of the logged in person */ - credentials: PropTypes.shape({ - /** The email/phone the user logged in with */ - login: PropTypes.string, - }), + credentials: OnyxEntry; }; -const defaultProps = { - credentials: {}, -}; +type EmailDeliveryFailurePageProps = EmailDeliveryFailurePageOnyxProps; -function EmailDeliveryFailurePage(props) { +function EmailDeliveryFailurePage({credentials}: EmailDeliveryFailurePageProps) { const styles = useThemeStyles(); const {isKeyboardShown} = useKeyboardState(); const {translate} = useLocalize(); - const login = Str.isSMSLogin(props.credentials.login) ? Str.removeSMSDomain(props.credentials.login) : props.credentials.login; + const login = useMemo(() => { + if (!credentials?.login) { + return ''; + } + return Str.isSMSLogin(credentials.login) ? Str.removeSMSDomain(credentials.login) : credentials.login; + }, [credentials?.login]); // This view doesn't have a field for user input, so dismiss the device keyboard if shown useEffect(() => { @@ -43,7 +42,7 @@ function EmailDeliveryFailurePage(props) { return ( <> - + {translate('emailDeliveryFailurePage.ourEmailProvider', {login})} @@ -89,10 +88,8 @@ function EmailDeliveryFailurePage(props) { ); } -EmailDeliveryFailurePage.propTypes = propTypes; -EmailDeliveryFailurePage.defaultProps = defaultProps; EmailDeliveryFailurePage.displayName = 'EmailDeliveryFailurePage'; -export default withOnyx({ +export default withOnyx({ credentials: {key: ONYXKEYS.CREDENTIALS}, })(EmailDeliveryFailurePage); diff --git a/src/pages/signin/GoogleSignInDesktopPage/index.website.tsx b/src/pages/signin/GoogleSignInDesktopPage/index.website.tsx index dda1a512e37d..b9451460dfb0 100644 --- a/src/pages/signin/GoogleSignInDesktopPage/index.website.tsx +++ b/src/pages/signin/GoogleSignInDesktopPage/index.website.tsx @@ -3,12 +3,7 @@ import ThirdPartySignInPage from '@pages/signin/ThirdPartySignInPage'; import CONST from '@src/CONST'; function GoogleSignInDesktopPage() { - return ( - - ); + return ; } export default GoogleSignInDesktopPage; diff --git a/src/pages/signin/Licenses.js b/src/pages/signin/Licenses.tsx similarity index 77% rename from src/pages/signin/Licenses.js rename to src/pages/signin/Licenses.tsx index 605cfed328b5..87271e3df6b2 100644 --- a/src/pages/signin/Licenses.js +++ b/src/pages/signin/Licenses.tsx @@ -3,25 +3,26 @@ import {View} from 'react-native'; import LocalePicker from '@components/LocalePicker'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; const currentYear = new Date().getFullYear(); -function Licenses(props) { +function Licenses() { const styles = useThemeStyles(); + const {translate} = useLocalize(); return ( <> {`© ${currentYear} Expensify`} - {props.translate('termsOfUse.phrase5')} + {translate('termsOfUse.phrase5')} {' '} - {props.translate('termsOfUse.phrase6')} + {translate('termsOfUse.phrase6')} . @@ -32,7 +33,6 @@ function Licenses(props) { ); } -Licenses.propTypes = {...withLocalizePropTypes}; Licenses.displayName = 'Licenses'; -export default withLocalize(Licenses); +export default Licenses; diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.tsx similarity index 70% rename from src/pages/signin/LoginForm/BaseLoginForm.js rename to src/pages/signin/LoginForm/BaseLoginForm.tsx index f5dc586dbc29..bca0fbd2f8ef 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx @@ -1,25 +1,25 @@ import {useIsFocused} from '@react-navigation/native'; import Str from 'expensify-common/lib/str'; -import PropTypes from 'prop-types'; +import type {ForwardedRef} 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 _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; import AppleSignIn from '@components/SignInButtons/AppleSignIn'; import GoogleSignIn from '@components/SignInButtons/GoogleSignIn'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import withToggleVisibilityView from '@components/withToggleVisibilityView'; +import type {WithToggleVisibilityViewProps} from '@components/withToggleVisibilityView'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; -import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import isInputAutoFilled from '@libs/isInputAutoFilled'; import Log from '@libs/Log'; @@ -34,80 +34,46 @@ import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; import * as Session from '@userActions/Session'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {CloseAccountForm} from '@src/types/form'; +import type {Account, Credentials} from '@src/types/onyx'; +import type LoginFormProps from './types'; +import type {InputHandle} from './types'; -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 */ - +type BaseLoginFormOnyxProps = { /** The details about the account that the user is signing in with */ - account: PropTypes.shape({ - /** An error message to display to the user */ - errors: PropTypes.objectOf(PropTypes.string), - - /** Success message to display when necessary */ - success: PropTypes.string, + account: OnyxEntry; - /** Whether a sign on form is loading (being submitted) */ - isLoading: PropTypes.bool, - }), - - closeAccount: PropTypes.shape({ - /** Message to display when user successfully closed their account */ - success: PropTypes.string, - }), + /** Message to display when user successfully closed their account */ + closeAccount: OnyxEntry; /** The credentials of the logged in person */ - credentials: PropTypes.shape({ - /** The email the user logged in with */ - login: PropTypes.string, - }), - - /** Props to detect online status */ - network: networkPropTypes.isRequired, - - isVisible: PropTypes.bool.isRequired, - - ...withLocalizePropTypes, + credentials: OnyxEntry; }; -const defaultProps = { - account: {}, - credentials: { - login: '', - }, - closeAccount: {}, - blurOnSubmit: false, - innerRef: () => {}, -}; +type BaseLoginFormProps = WithToggleVisibilityViewProps & BaseLoginFormOnyxProps & LoginFormProps; const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); -function LoginForm(props) { +function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false, isVisible}: BaseLoginFormProps, ref: ForwardedRef) { const styles = useThemeStyles(); - const input = useRef(); - const [login, setLogin] = useState(() => Str.removeSMSDomain(props.credentials.login || '')); - const [formError, setFormError] = useState(false); - const prevIsVisible = usePrevious(props.isVisible); + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + const input = useRef(null); + const [login, setLogin] = useState(() => Str.removeSMSDomain(credentials?.login ?? '')); + const [formError, setFormError] = useState(); + const prevIsVisible = usePrevious(isVisible); const firstBlurred = useRef(false); const isFocused = useIsFocused(); const isLoading = useRef(false); const {shouldUseNarrowLayout, isInModal} = useResponsiveLayout(); - const {translate} = props; - /** * Validate the input value and set the error for formError - * - * @param {String} value */ const validate = useCallback( - (value) => { + (value: string) => { const loginTrim = value.trim(); if (!loginTrim) { setFormError('common.pleaseEnterEmailOrPhoneNumber'); @@ -126,7 +92,7 @@ function LoginForm(props) { return false; } - setFormError(null); + setFormError(undefined); return true; }, [setFormError], @@ -134,26 +100,24 @@ function LoginForm(props) { /** * Handle text input and validate the text input if it is blurred - * - * @param {String} text */ const onTextInput = useCallback( - (text) => { + (text: string) => { setLogin(text); if (firstBlurred.current) { validate(text); } - if (props.account.errors || props.account.message) { + if (!!account?.errors || !!account?.message) { Session.clearAccountMessages(); } // Clear the "Account successfully closed" message when the user starts typing - if (props.closeAccount.success && !isInputAutoFilled(input.current)) { + if (closeAccount?.success && !isInputAutoFilled(input.current)) { CloseAccount.setDefaultData(); } }, - [props.account, props.closeAccount, input, setLogin, validate], + [account, closeAccount, input, setLogin, validate], ); function getSignInWithStyles() { @@ -164,13 +128,13 @@ function LoginForm(props) { * Check that all the form fields are valid, then trigger the submit callback */ const validateAndSubmitForm = useCallback(() => { - if (props.network.isOffline || props.account.isLoading || isLoading.current) { + if (!!isOffline || !!account?.isLoading || isLoading.current) { return; } isLoading.current = true; // If account was closed and have success message in Onyx, we clear it here - if (!_.isEmpty(props.closeAccount.success)) { + if (closeAccount?.success) { CloseAccount.setDefaultData(); } @@ -197,22 +161,23 @@ function LoginForm(props) { const parsedPhoneNumber = parsePhoneNumber(phoneLogin); // Check if this login has an account associated with it or not - Session.beginSignIn(parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : loginTrim); - }, [login, props.account, props.closeAccount, props.network, validate]); + Session.beginSignIn(parsedPhoneNumber.possible && parsedPhoneNumber.number?.e164 ? parsedPhoneNumber.number.e164 : loginTrim); + }, [login, account, closeAccount, isOffline, validate]); useEffect(() => { - // Just call clearAccountMessages on the login page (home route), because when the user is in the transition route and not yet authenticated, - // this component will also be mounted, resetting account.isLoading will cause the app to briefly display the session expiration page. + // Call clearAccountMessages on the login page (home route). + // When the user is in the transition route and not yet authenticated, this component will also be mounted, + // resetting account.isLoading will cause the app to briefly display the session expiration page. - if (isFocused && props.isVisible) { + if (isFocused && isVisible) { Session.clearAccountMessages(); } - if (!canFocusInputOnScreenFocus() || !input.current || !props.isVisible || !isFocused) { + if (!canFocusInputOnScreenFocus() || !input.current || !isVisible || !isFocused) { return; } - let focusTimeout; + let focusTimeout: NodeJS.Timeout; if (isInModal) { - focusTimeout = setTimeout(() => input.current.focus(), CONST.ANIMATED_TRANSITION); + focusTimeout = setTimeout(() => input.current?.focus(), CONST.ANIMATED_TRANSITION); } else { input.current.focus(); } @@ -221,27 +186,30 @@ function LoginForm(props) { }, []); useEffect(() => { - if (props.account.isLoading !== false) { + if (account?.isLoading !== false) { return; } isLoading.current = false; - }, [props.account.isLoading]); + }, [account?.isLoading]); useEffect(() => { - if (props.blurOnSubmit) { - input.current.blur(); + if (blurOnSubmit) { + input.current?.blur(); } // Only focus the input if the form becomes visible again, to prevent the keyboard from automatically opening on touchscreen devices after signing out - if (!input.current || prevIsVisible || !props.isVisible) { + if (!input.current || prevIsVisible || !isVisible) { return; } - input.current.focus(); - }, [props.blurOnSubmit, props.isVisible, prevIsVisible]); + input.current?.focus(); + }, [blurOnSubmit, isVisible, prevIsVisible]); - useImperativeHandle(props.innerRef, () => ({ + useImperativeHandle(ref, () => ({ isInputFocused() { - return input.current && input.current.isFocused(); + if (!input.current) { + return false; + } + return input.current.isFocused() as boolean; }, clearDataAndFocus(clearLogin = true) { if (!input.current) { @@ -254,8 +222,8 @@ function LoginForm(props) { }, })); - const serverErrorText = useMemo(() => ErrorUtils.getLatestErrorMessage(props.account), [props.account]); - const shouldShowServerError = !_.isEmpty(serverErrorText) && _.isEmpty(formError); + const serverErrorText = useMemo(() => (account ? ErrorUtils.getLatestErrorMessage(account) : ''), [account]); + const shouldShowServerError = !!serverErrorText && !formError; return ( <> @@ -296,27 +264,28 @@ function LoginForm(props) { autoCapitalize="none" autoCorrect={false} inputMode={CONST.INPUT_MODE.EMAIL} - errorText={formError || ''} + errorText={formError} hasError={shouldShowServerError} maxLength={CONST.LOGIN_CHARACTER_LIMIT} /> - {!_.isEmpty(props.account.success) && {props.account.success}} - {!_.isEmpty(props.closeAccount.success || props.account.message) && ( + {!!account?.success && {account.success}} + {(!!closeAccount?.success || !!account?.message) && ( )} { // We need to unmount the submit button when the component is not visible so that the Enter button // key handler gets unsubscribed - props.isVisible && ( + isVisible && ( - {props.translate('common.signInWith')} + {translate('common.signInWith')} @@ -356,27 +325,12 @@ function LoginForm(props) { ); } -LoginForm.propTypes = propTypes; -LoginForm.defaultProps = defaultProps; -LoginForm.displayName = 'LoginForm'; - -const LoginFormWithRef = forwardRef((props, ref) => ( - -)); - -LoginFormWithRef.displayName = 'LoginFormWithRef'; +BaseLoginForm.displayName = 'BaseLoginForm'; -export default compose( - withOnyx({ +export default withToggleVisibilityView( + withOnyx({ account: {key: ONYXKEYS.ACCOUNT}, credentials: {key: ONYXKEYS.CREDENTIALS}, closeAccount: {key: ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM}, - }), - withLocalize, - withToggleVisibilityView, - withNetwork(), -)(LoginFormWithRef); + })(forwardRef(BaseLoginForm)), +); diff --git a/src/pages/signin/LoginForm/index.js b/src/pages/signin/LoginForm/index.js deleted file mode 100644 index e861100c25fe..000000000000 --- a/src/pages/signin/LoginForm/index.js +++ /dev/null @@ -1,42 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import refPropTypes from '@components/refPropTypes'; -import BaseLoginForm from './BaseLoginForm'; - -const propTypes = { - /** Function used to scroll to the top of the page */ - scrollPageToTop: PropTypes.func, - - /** A reference so we can expose clearDataAndFocus */ - innerRef: refPropTypes, -}; -const defaultProps = { - scrollPageToTop: undefined, - innerRef: () => {}, -}; - -function LoginForm({innerRef, ...props}) { - return ( - - ); -} - -LoginForm.displayName = 'LoginForm'; -LoginForm.propTypes = propTypes; -LoginForm.defaultProps = defaultProps; - -const LoginFormWithRef = React.forwardRef((props, ref) => ( - -)); - -LoginFormWithRef.displayName = 'LoginFormWithRef'; - -export default LoginFormWithRef; diff --git a/src/pages/signin/LoginForm/index.native.js b/src/pages/signin/LoginForm/index.native.js deleted file mode 100644 index 3187faac8127..000000000000 --- a/src/pages/signin/LoginForm/index.native.js +++ /dev/null @@ -1,72 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useEffect, useRef} from 'react'; -import _ from 'underscore'; -import refPropTypes from '@components/refPropTypes'; -import AppStateMonitor from '@libs/AppStateMonitor'; -import BaseLoginForm from './BaseLoginForm'; - -const propTypes = { - /** Function used to scroll to the top of the page */ - scrollPageToTop: PropTypes.func, - - /** A reference so we can expose clearDataAndFocus */ - innerRef: refPropTypes, -}; -const defaultProps = { - scrollPageToTop: undefined, - innerRef: () => {}, -}; - -function LoginForm({innerRef, ...props}) { - 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 ( - { - loginFormRef.current = ref; - if (typeof innerRef === 'function') { - innerRef(ref); - } else if (innerRef && _.has(innerRef, 'current')) { - // eslint-disable-next-line no-param-reassign - innerRef.current = ref; - } - }} - /> - ); -} - -LoginForm.displayName = 'LoginForm'; -LoginForm.propTypes = propTypes; -LoginForm.defaultProps = defaultProps; - -const LoginFormWithRef = React.forwardRef((props, ref) => ( - -)); - -LoginFormWithRef.displayName = 'LoginFormWithRef'; - -export default LoginFormWithRef; diff --git a/src/pages/signin/LoginForm/index.native.tsx b/src/pages/signin/LoginForm/index.native.tsx new file mode 100644 index 000000000000..6d8f771810e7 --- /dev/null +++ b/src/pages/signin/LoginForm/index.native.tsx @@ -0,0 +1,43 @@ +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useEffect, useImperativeHandle, useRef} from 'react'; +import AppStateMonitor from '@libs/AppStateMonitor'; +import BaseLoginForm from './BaseLoginForm'; +import type {InputHandle} from './types'; +import type LoginFormProps from './types'; + +function LoginForm({scrollPageToTop, ...rest}: LoginFormProps, ref: ForwardedRef) { + const loginFormRef = useRef(); + + useImperativeHandle(ref, () => ({ + isInputFocused: loginFormRef.current ? loginFormRef.current.isInputFocused : () => false, + clearDataAndFocus: loginFormRef.current ? loginFormRef.current?.clearDataAndFocus : () => null, + })); + + useEffect(() => { + if (!scrollPageToTop) { + return; + } + + return AppStateMonitor.addBecameActiveListener(() => { + const isInputFocused = loginFormRef.current?.isInputFocused(); + if (!isInputFocused) { + return; + } + + scrollPageToTop(); + }); + }, [scrollPageToTop]); + + return ( + + ); +} + +LoginForm.displayName = 'LoginForm'; + +export default forwardRef(LoginForm); diff --git a/src/pages/signin/LoginForm/index.tsx b/src/pages/signin/LoginForm/index.tsx new file mode 100644 index 000000000000..1dfe990f67c1 --- /dev/null +++ b/src/pages/signin/LoginForm/index.tsx @@ -0,0 +1,19 @@ +import type {ForwardedRef} from 'react'; +import React, {forwardRef} from 'react'; +import BaseLoginForm from './BaseLoginForm'; +import type {InputHandle} from './types'; +import type LoginFormProps from './types'; + +function LoginForm(props: LoginFormProps, ref: ForwardedRef) { + return ( + + ); +} + +LoginForm.displayName = 'LoginForm'; + +export default forwardRef(LoginForm); diff --git a/src/pages/signin/LoginForm/types.ts b/src/pages/signin/LoginForm/types.ts new file mode 100644 index 000000000000..775009072a2d --- /dev/null +++ b/src/pages/signin/LoginForm/types.ts @@ -0,0 +1,19 @@ +type LoginFormProps = { + /** Function used to scroll to the top of the page */ + scrollPageToTop?: () => void; + + /** Should we dismiss the keyboard when transitioning away from the page? */ + blurOnSubmit?: boolean; + + /** Whether the content is visible. */ + isVisible: boolean; +}; + +type InputHandle = { + isInputFocused: () => boolean; + clearDataAndFocus: (clearLogin?: boolean) => void; +}; + +export type {InputHandle}; + +export default LoginFormProps; diff --git a/src/pages/signin/SAMLSignInPage/index.js b/src/pages/signin/SAMLSignInPage/index.js deleted file mode 100644 index ec3cc01197bd..000000000000 --- a/src/pages/signin/SAMLSignInPage/index.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useEffect} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import SAMLLoadingIndicator from '@components/SAMLLoadingIndicator'; -import CONFIG from '@src/CONFIG'; -import ONYXKEYS from '@src/ONYXKEYS'; - -const propTypes = { - /** The credentials of the logged in person */ - credentials: PropTypes.shape({ - /** The email/phone the user logged in with */ - login: PropTypes.string, - }), -}; - -const defaultProps = { - credentials: {}, -}; - -function SAMLSignInPage({credentials}) { - useEffect(() => { - window.open(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`, '_self'); - }, [credentials.login]); - - return ; -} - -SAMLSignInPage.propTypes = propTypes; -SAMLSignInPage.defaultProps = defaultProps; -SAMLSignInPage.displayName = 'SAMLSignInPage'; - -export default withOnyx({ - credentials: {key: ONYXKEYS.CREDENTIALS}, -})(SAMLSignInPage); diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.tsx similarity index 74% rename from src/pages/signin/SAMLSignInPage/index.native.js rename to src/pages/signin/SAMLSignInPage/index.native.tsx index 9fe60e56353e..498742a76144 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.js +++ b/src/pages/signin/SAMLSignInPage/index.native.tsx @@ -1,7 +1,7 @@ -import PropTypes from 'prop-types'; import React, {useCallback, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import WebView from 'react-native-webview'; +import type {WebViewNativeEvent} from 'react-native-webview/lib/WebViewTypes'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import SAMLLoadingIndicator from '@components/SAMLLoadingIndicator'; @@ -13,58 +13,38 @@ import * as Session from '@userActions/Session'; import CONFIG from '@src/CONFIG'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {SAMLSignInPageOnyxProps, SAMLSignInPageProps} from './types'; -const propTypes = { - /** The credentials of the logged in person */ - credentials: PropTypes.shape({ - /** The email/phone the user logged in with */ - login: PropTypes.string, - }), - - /** State of the logging in user's account */ - account: PropTypes.shape({ - /** Whether the account is loading */ - isLoading: PropTypes.bool, - }), -}; - -const defaultProps = { - credentials: {}, - account: {}, -}; - -function SAMLSignInPage({credentials, account}) { - const samlLoginURL = `${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}&platform=${getPlatform()}`; +function SAMLSignInPage({credentials, account}: SAMLSignInPageProps) { + const samlLoginURL = `${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials?.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}&platform=${getPlatform()}`; const [showNavigation, shouldShowNavigation] = useState(true); /** * Handles in-app navigation once we get a response back from Expensify - * - * @param {String} params.url */ const handleNavigationStateChange = useCallback( - ({url}) => { + ({url}: WebViewNativeEvent) => { Log.info('SAMLSignInPage - Handling SAML navigation change'); - // If we've gotten a callback then remove the option to navigate back to the sign in page + // If we've gotten a callback then remove the option to navigate back to the sign-in page if (url.includes('loginCallback')) { shouldShowNavigation(false); } const searchParams = new URLSearchParams(new URL(url).search); - if (searchParams.has('shortLivedAuthToken') && !account.isLoading) { + const shortLivedAuthToken = searchParams.get('shortLivedAuthToken'); + if (!account?.isLoading && credentials?.login && !!shortLivedAuthToken) { Log.info('SAMLSignInPage - Successfully received shortLivedAuthToken. Signing in...'); - const shortLivedAuthToken = searchParams.get('shortLivedAuthToken'); Session.signInWithShortLivedAuthToken(credentials.login, shortLivedAuthToken); } // If the login attempt is unsuccessful, set the error message for the account and redirect to sign in page if (searchParams.has('error')) { Session.clearSignInData(); - Session.setAccountError(searchParams.get('error')); + Session.setAccountError(searchParams.get('error') ?? ''); Navigation.navigate(ROUTES.HOME); } }, - [credentials.login, shouldShowNavigation, account.isLoading], + [credentials?.login, shouldShowNavigation, account?.isLoading], ); return ( @@ -96,11 +76,9 @@ function SAMLSignInPage({credentials, account}) { ); } -SAMLSignInPage.propTypes = propTypes; -SAMLSignInPage.defaultProps = defaultProps; SAMLSignInPage.displayName = 'SAMLSignInPage'; -export default withOnyx({ +export default withOnyx({ credentials: {key: ONYXKEYS.CREDENTIALS}, account: {key: ONYXKEYS.ACCOUNT}, })(SAMLSignInPage); diff --git a/src/pages/signin/SAMLSignInPage/index.tsx b/src/pages/signin/SAMLSignInPage/index.tsx new file mode 100644 index 000000000000..701c2917bea6 --- /dev/null +++ b/src/pages/signin/SAMLSignInPage/index.tsx @@ -0,0 +1,21 @@ +import React, {useEffect} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import SAMLLoadingIndicator from '@components/SAMLLoadingIndicator'; +import CONFIG from '@src/CONFIG'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {SAMLSignInPageOnyxProps, SAMLSignInPageProps} from './types'; + +function SAMLSignInPage({credentials}: SAMLSignInPageProps) { + useEffect(() => { + window.open(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials?.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`, '_self'); + }, [credentials?.login]); + + return ; +} + +SAMLSignInPage.displayName = 'SAMLSignInPage'; + +export default withOnyx({ + account: {key: ONYXKEYS.ACCOUNT}, + credentials: {key: ONYXKEYS.CREDENTIALS}, +})(SAMLSignInPage); diff --git a/src/pages/signin/SAMLSignInPage/types.ts b/src/pages/signin/SAMLSignInPage/types.ts new file mode 100644 index 000000000000..f8b99e8b91f2 --- /dev/null +++ b/src/pages/signin/SAMLSignInPage/types.ts @@ -0,0 +1,14 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type {Account, Credentials} from '@src/types/onyx'; + +type SAMLSignInPageOnyxProps = { + /** The credentials of the logged in person */ + credentials: OnyxEntry; + + /** State of the logging in user's account */ + account: OnyxEntry; +}; + +type SAMLSignInPageProps = SAMLSignInPageOnyxProps; + +export type {SAMLSignInPageProps, SAMLSignInPageOnyxProps}; diff --git a/src/pages/signin/SignInHeroCopy.js b/src/pages/signin/SignInHeroCopy.js deleted file mode 100644 index 847de3868cee..000000000000 --- a/src/pages/signin/SignInHeroCopy.js +++ /dev/null @@ -1,51 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import variables from '@styles/variables'; - -const propTypes = { - /** Override the green headline copy */ - customHeadline: PropTypes.string, - - /** Override the smaller hero body copy below the headline */ - customHeroBody: PropTypes.string, - - ...windowDimensionsPropTypes, - ...withLocalizePropTypes, -}; - -const defaultProps = { - customHeadline: '', - customHeroBody: '', -}; - -function SignInHeroCopy(props) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - return ( - - - {props.customHeadline || props.translate('login.hero.header')} - - {props.customHeroBody || props.translate('login.hero.body')} - - ); -} - -SignInHeroCopy.displayName = 'SignInHeroCopy'; -SignInHeroCopy.propTypes = propTypes; -SignInHeroCopy.defaultProps = defaultProps; - -export default compose(withWindowDimensions, withLocalize)(SignInHeroCopy); diff --git a/src/pages/signin/SignInModal.js b/src/pages/signin/SignInModal.tsx similarity index 86% rename from src/pages/signin/SignInModal.js rename to src/pages/signin/SignInModal.tsx index 006259756685..b2ee9d218da9 100644 --- a/src/pages/signin/SignInModal.js +++ b/src/pages/signin/SignInModal.tsx @@ -8,16 +8,12 @@ import * as Session from '@userActions/Session'; import SCREENS from '@src/SCREENS'; import SignInPage from './SignInPage'; -const propTypes = {}; - -const defaultProps = {}; - function SignInModal() { const theme = useTheme(); const StyleUtils = useStyleUtils(); if (!Session.isAnonymousUser()) { - // Sign in in RHP is only for anonymous users + // Signing in RHP is only for anonymous users Navigation.isNavigationReady().then(() => { Navigation.dismissModal(); }); @@ -35,8 +31,6 @@ function SignInModal() { ); } -SignInModal.propTypes = propTypes; -SignInModal.defaultProps = defaultProps; SignInModal.displayName = 'SignInModal'; export default SignInModal; diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.tsx similarity index 75% rename from src/pages/signin/SignInPage.js rename to src/pages/signin/SignInPage.tsx index 3e2d6c7082de..3d761601a919 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.tsx @@ -1,9 +1,8 @@ import Str from 'expensify-common/lib/str'; -import PropTypes from 'prop-types'; import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import CustomStatusBarAndBackground from '@components/CustomStatusBarAndBackground'; import ThemeProvider from '@components/ThemeProvider'; @@ -24,83 +23,79 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Account, Credentials, Locale} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ChooseSSOOrMagicCode from './ChooseSSOOrMagicCode'; import EmailDeliveryFailurePage from './EmailDeliveryFailurePage'; import LoginForm from './LoginForm'; +import type {InputHandle} from './LoginForm/types'; import SignInPageLayout from './SignInPageLayout'; +import type {SignInPageLayoutRef} from './SignInPageLayout/types'; import UnlinkLoginForm from './UnlinkLoginForm'; import ValidateCodeForm from './ValidateCodeForm'; -const propTypes = { +type SignInPageInnerOnyxProps = { /** The details about the account that the user is signing in with */ - account: PropTypes.shape({ - /** Error to display when there is an account error returned */ - errors: PropTypes.objectOf(PropTypes.string), - - /** Whether the account is validated */ - validated: PropTypes.bool, - - /** The primaryLogin associated with the account */ - primaryLogin: PropTypes.string, - - /** Does this account require 2FA? */ - requiresTwoFactorAuth: PropTypes.bool, - - /** Is this account having trouble receiving emails */ - hasEmailDeliveryFailure: PropTypes.bool, - - /** Whether or not a sign on form is loading (being submitted) */ - isLoading: PropTypes.bool, - - /** Form that is being loaded */ - loadingForm: PropTypes.oneOf(_.values(CONST.FORMS)), - - /** Whether or not the user has SAML enabled on their account */ - isSAMLEnabled: PropTypes.bool, - - /** Whether or not SAML is required on the account */ - isSAMLRequired: PropTypes.bool, - }), + account: OnyxEntry; /** The credentials of the person signing in */ - credentials: PropTypes.shape({ - login: PropTypes.string, - twoFactorAuthCode: PropTypes.string, - validateCode: PropTypes.string, - }), + credentials: OnyxEntry; /** Active Clients connected to ONYX Database */ - activeClients: PropTypes.arrayOf(PropTypes.string), + activeClients: OnyxEntry; /** The user's preferred locale */ - preferredLocale: PropTypes.string, + preferredLocale: OnyxEntry; +}; + +type SignInPageInnerProps = SignInPageInnerOnyxProps; + +type RenderOption = { + shouldShowLoginForm: boolean; + shouldShowEmailDeliveryFailurePage: boolean; + shouldShowUnlinkLoginForm: boolean; + shouldShowValidateCodeForm: boolean; + shouldShowChooseSSOOrMagicCode: boolean; + shouldInitiateSAMLLogin: boolean; + shouldShowWelcomeHeader: boolean; + shouldShowWelcomeText: boolean; }; -const defaultProps = { - account: {}, - credentials: {}, - activeClients: [], - preferredLocale: '', +type GetRenderOptionsParams = { + hasLogin: boolean; + hasValidateCode: boolean; + account: OnyxEntry; + isPrimaryLogin: boolean; + isUsingMagicCode: boolean; + hasInitiatedSAMLLogin: boolean; + shouldShowAnotherLoginPageOpenedMessage: boolean; }; /** - * @param {Boolean} hasLogin - * @param {Boolean} hasValidateCode - * @param {Object} account - * @param {Boolean} isPrimaryLogin - * @param {Boolean} isUsingMagicCode - * @param {Boolean} hasInitiatedSAMLLogin - * @param {Boolean} hasEmailDeliveryFailure - * @returns {Object} + * @param hasLogin + * @param hasValidateCode + * @param account + * @param isPrimaryLogin + * @param isUsingMagicCode + * @param hasInitiatedSAMLLogin + * @param hasEmailDeliveryFailure */ -function getRenderOptions({hasLogin, hasValidateCode, account, isPrimaryLogin, isUsingMagicCode, hasInitiatedSAMLLogin, shouldShowAnotherLoginPageOpenedMessage}) { - const hasAccount = !_.isEmpty(account); - const isSAMLEnabled = Boolean(account.isSAMLEnabled); - const isSAMLRequired = Boolean(account.isSAMLRequired); - const hasEmailDeliveryFailure = Boolean(account.hasEmailDeliveryFailure); - - // True if the user has SAML required and we haven't already initiated SAML for their account - const shouldInitiateSAMLLogin = hasAccount && hasLogin && isSAMLRequired && !hasInitiatedSAMLLogin && account.isLoading; +function getRenderOptions({ + hasLogin, + hasValidateCode, + account, + isPrimaryLogin, + isUsingMagicCode, + hasInitiatedSAMLLogin, + shouldShowAnotherLoginPageOpenedMessage, +}: GetRenderOptionsParams): RenderOption { + const hasAccount = !isEmptyObject(account); + const isSAMLEnabled = !!account?.isSAMLEnabled; + const isSAMLRequired = !!account?.isSAMLRequired; + const hasEmailDeliveryFailure = !!account?.hasEmailDeliveryFailure; + + // True, if the user has SAML required, and we haven't yet initiated SAML for their account + const shouldInitiateSAMLLogin = hasAccount && hasLogin && isSAMLRequired && !hasInitiatedSAMLLogin && !!account.isLoading; const shouldShowChooseSSOOrMagicCode = hasAccount && hasLogin && isSAMLEnabled && !isSAMLRequired && !isUsingMagicCode; // SAML required users may reload the login page after having already entered their login details, in which @@ -112,7 +107,7 @@ function getRenderOptions({hasLogin, hasValidateCode, account, isPrimaryLogin, i const shouldShowLoginForm = !shouldShowAnotherLoginPageOpenedMessage && !hasLogin && !hasValidateCode; const shouldShowEmailDeliveryFailurePage = hasLogin && hasEmailDeliveryFailure && !shouldShowChooseSSOOrMagicCode && !shouldInitiateSAMLLogin; - const isUnvalidatedSecondaryLogin = hasLogin && !isPrimaryLogin && !account.validated && !hasEmailDeliveryFailure; + const isUnvalidatedSecondaryLogin = hasLogin && !isPrimaryLogin && !account?.validated && !hasEmailDeliveryFailure; const shouldShowValidateCodeForm = hasAccount && (hasLogin || hasValidateCode) && !isUnvalidatedSecondaryLogin && !hasEmailDeliveryFailure && !shouldShowChooseSSOOrMagicCode && !isSAMLRequired; const shouldShowWelcomeHeader = shouldShowLoginForm || shouldShowValidateCodeForm || shouldShowChooseSSOOrMagicCode || isUnvalidatedSecondaryLogin; @@ -129,14 +124,14 @@ function getRenderOptions({hasLogin, hasValidateCode, account, isPrimaryLogin, i }; } -function SignInPageInner({credentials, account, activeClients, preferredLocale}) { +function SignInPageInner({credentials, account, activeClients = [], preferredLocale}: SignInPageInnerProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate, formatPhoneNumber} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const safeAreaInsets = useSafeAreaInsets(); - const signInPageLayoutRef = useRef(); - const loginFormRef = useRef(); + const signInPageLayoutRef = useRef(null); + const loginFormRef = useRef(null); /** This state is needed to keep track of if user is using recovery code instead of 2fa code, * and we need it here since welcome text(`welcomeText`) also depends on it */ const [isUsingRecoveryCode, setIsUsingRecoveryCode] = useState(false); @@ -149,7 +144,7 @@ function SignInPageInner({credentials, account, activeClients, preferredLocale}) * if we need to clear their sign in details so they can enter a login */ const [hasInitiatedSAMLLogin, setHasInitiatedSAMLLogin] = useState(false); - const isClientTheLeader = activeClients && ActiveClientManager.isClientTheLeader(); + const isClientTheLeader = !!activeClients && ActiveClientManager.isClientTheLeader(); // We need to show "Another login page is opened" message if the page isn't active and visible // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowAnotherLoginPageOpenedMessage = Visibility.isVisible() && !isClientTheLeader; @@ -162,7 +157,7 @@ function SignInPageInner({credentials, account, activeClients, preferredLocale}) App.setLocale(Localize.getDevicePreferredLocale()); }, [preferredLocale]); useEffect(() => { - if (credentials.login) { + if (credentials?.login) { return; } @@ -173,7 +168,7 @@ function SignInPageInner({credentials, account, activeClients, preferredLocale}) if (hasInitiatedSAMLLogin) { setHasInitiatedSAMLLogin(false); } - }, [credentials.login, isUsingMagicCode, setIsUsingMagicCode, hasInitiatedSAMLLogin, setHasInitiatedSAMLLogin]); + }, [credentials?.login, isUsingMagicCode, setIsUsingMagicCode, hasInitiatedSAMLLogin, setHasInitiatedSAMLLogin]); const { shouldShowLoginForm, @@ -185,10 +180,10 @@ function SignInPageInner({credentials, account, activeClients, preferredLocale}) shouldShowWelcomeHeader, shouldShowWelcomeText, } = getRenderOptions({ - hasLogin: Boolean(credentials.login), - hasValidateCode: Boolean(credentials.validateCode), + hasLogin: !!credentials?.login, + hasValidateCode: !!credentials?.validateCode, account, - isPrimaryLogin: !account.primaryLogin || account.primaryLogin === credentials.login, + isPrimaryLogin: !account?.primaryLogin || account.primaryLogin === credentials?.login, isUsingMagicCode, hasInitiatedSAMLLogin, shouldShowAnotherLoginPageOpenedMessage, @@ -210,16 +205,16 @@ function SignInPageInner({credentials, account, activeClients, preferredLocale}) welcomeHeader = shouldUseNarrowLayout ? headerText : translate('welcomeText.getStarted'); welcomeText = shouldUseNarrowLayout ? translate('welcomeText.getStarted') : ''; } else if (shouldShowValidateCodeForm) { - if (account.requiresTwoFactorAuth) { + if (account?.requiresTwoFactorAuth) { // We will only know this after a user signs in successfully, without their 2FA code welcomeHeader = shouldUseNarrowLayout ? '' : translate('welcomeText.welcomeBack'); welcomeText = isUsingRecoveryCode ? translate('validateCodeForm.enterRecoveryCode') : translate('validateCodeForm.enterAuthenticatorCode'); } else { - const userLogin = Str.removeSMSDomain(credentials.login || ''); + const userLogin = Str.removeSMSDomain(credentials?.login ?? ''); // replacing spaces with "hard spaces" to prevent breaking the number const userLoginToDisplay = Str.isSMSLogin(userLogin) ? formatPhoneNumber(userLogin).replace(/ /g, '\u00A0') : userLogin; - if (account.validated) { + if (account?.validated) { welcomeHeader = shouldUseNarrowLayout ? '' : translate('welcomeText.welcomeBack'); welcomeText = shouldUseNarrowLayout ? `${translate('welcomeText.welcomeBack')} ${translate('welcomeText.welcomeEnterMagicCode', {login: userLoginToDisplay})}` @@ -243,8 +238,8 @@ function SignInPageInner({credentials, account, activeClients, preferredLocale}) } const navigateFocus = () => { - signInPageLayoutRef.current.scrollPageToTop(); - loginFormRef.current.clearDataAndFocus(); + signInPageLayoutRef.current?.scrollPageToTop(); + loginFormRef.current?.clearDataAndFocus(); }; return ( @@ -267,11 +262,12 @@ function SignInPageInner({credentials, account, activeClients, preferredLocale}) {shouldShowValidateCodeForm && ( ); } -SignInPageInner.propTypes = propTypes; -SignInPageInner.defaultProps = defaultProps; + SignInPageInner.displayName = 'SignInPage'; -function SignInPage(props) { +type SignInPageProps = SignInPageInnerProps; +type SignInPageOnyxProps = SignInPageInnerOnyxProps; + +function SignInPage(props: SignInPageProps) { return ( @@ -308,16 +306,16 @@ function SignInPage(props) { ); } -export default withOnyx({ +export default withOnyx({ account: {key: ONYXKEYS.ACCOUNT}, credentials: {key: ONYXKEYS.CREDENTIALS}, /** - This variable is only added to make sure the component is re-rendered - whenever the activeClients change, so that we call the - ActiveClientManager.isClientTheLeader function - everytime the leader client changes. - We use that function to prevent repeating code that checks which client is the leader. - */ + This variable is only added to make sure the component is re-rendered + whenever the activeClients change, so that we call the + ActiveClientManager.isClientTheLeader function + everytime the leader client changes. + We use that function to prevent repeating code that checks which client is the leader. + */ activeClients: {key: ONYXKEYS.ACTIVE_CLIENTS}, preferredLocale: { key: ONYXKEYS.NVP_PREFERRED_LOCALE, diff --git a/src/pages/signin/SignInPageHero.js b/src/pages/signin/SignInPageHero.js deleted file mode 100644 index 81415452451e..000000000000 --- a/src/pages/signin/SignInPageHero.js +++ /dev/null @@ -1,55 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import variables from '@styles/variables'; -import SignInHeroCopy from './SignInHeroCopy'; -import SignInHeroImage from './SignInHeroImage'; - -const propTypes = { - /** Override the green headline copy */ - customHeadline: PropTypes.string, - - /** Override the smaller hero body copy below the headline */ - customHeroBody: PropTypes.string, - - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - customHeadline: '', - customHeroBody: '', -}; - -function SignInPageHero(props) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - return ( - - - - - - - ); -} - -SignInPageHero.displayName = 'SignInPageHero'; -SignInPageHero.propTypes = propTypes; -SignInPageHero.defaultProps = defaultProps; - -export default withWindowDimensions(SignInPageHero); diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.android.js b/src/pages/signin/SignInPageLayout/BackgroundImage/index.android.js deleted file mode 100644 index 8d261134baba..000000000000 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.android.js +++ /dev/null @@ -1,22 +0,0 @@ -import {Image} from 'expo-image'; -import React from 'react'; -import AndroidBackgroundImage from '@assets/images/home-background--android.svg'; -import useThemeStyles from '@hooks/useThemeStyles'; -import defaultPropTypes from './propTypes'; - -function BackgroundImage(props) { - const styles = useThemeStyles(); - return ( - - ); -} - -BackgroundImage.displayName = 'BackgroundImage'; -BackgroundImage.propTypes = defaultPropTypes; - -export default BackgroundImage; diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.android.tsx b/src/pages/signin/SignInPageLayout/BackgroundImage/index.android.tsx new file mode 100644 index 000000000000..8e7c1b7d7c38 --- /dev/null +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.android.tsx @@ -0,0 +1,22 @@ +import {Image} from 'expo-image'; +import React from 'react'; +import type {ImageSourcePropType} from 'react-native'; +import AndroidBackgroundImage from '@assets/images/home-background--android.svg'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type BackgroundImageProps from './types'; + +function BackgroundImage({pointerEvents, width, transitionDuration}: BackgroundImageProps) { + const styles = useThemeStyles(); + return ( + + ); +} + +BackgroundImage.displayName = 'BackgroundImage'; + +export default BackgroundImage; diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx similarity index 69% rename from src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js rename to src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx index c0b907fb7fc9..b52709951c80 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx @@ -1,6 +1,6 @@ import {Image} from 'expo-image'; -import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; +import type {ImageSourcePropType} from 'react-native'; import Reanimated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import DesktopBackgroundImage from '@assets/images/home-background--desktop.svg'; import MobileBackgroundImage from '@assets/images/home-background--mobile-new.svg'; @@ -8,22 +8,12 @@ import useIsSplashHidden from '@hooks/useIsSplashHidden'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -import defaultPropTypes from './propTypes'; +import type BackgroundImageProps from './types'; -const defaultProps = { - isSmallScreen: false, -}; - -const propTypes = { - /** Is the window width narrow, like on a mobile device */ - isSmallScreen: PropTypes.bool, - - ...defaultPropTypes, -}; -function BackgroundImage(props) { +function BackgroundImage({width, transitionDuration, isSmallScreen = false}: BackgroundImageProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const src = useMemo(() => (props.isSmallScreen ? MobileBackgroundImage : DesktopBackgroundImage), [props.isSmallScreen]); + const src = useMemo(() => (isSmallScreen ? MobileBackgroundImage : DesktopBackgroundImage), [isSmallScreen]); const opacity = useSharedValue(0); const animatedStyle = useAnimatedStyle(() => ({opacity: opacity.value})); @@ -43,19 +33,17 @@ function BackgroundImage(props) { } return ( - + setOpacityAnimation()} - style={[styles.signInBackground, StyleUtils.getWidthStyle(props.width)]} - transition={props.transitionDuration} + style={[styles.signInBackground, StyleUtils.getWidthStyle(width)]} + transition={transitionDuration} /> ); } BackgroundImage.displayName = 'BackgroundImage'; -BackgroundImage.propTypes = propTypes; -BackgroundImage.defaultProps = defaultProps; export default BackgroundImage; diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.js b/src/pages/signin/SignInPageLayout/BackgroundImage/index.tsx similarity index 63% rename from src/pages/signin/SignInPageLayout/BackgroundImage/index.js rename to src/pages/signin/SignInPageLayout/BackgroundImage/index.tsx index 96406d6f3d31..b1e25c1cfdbf 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.js +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.tsx @@ -1,22 +1,11 @@ -import PropTypes from 'prop-types'; import React from 'react'; import * as Animatable from 'react-native-animatable'; import DesktopBackgroundImage from '@assets/images/home-background--desktop.svg'; import MobileBackgroundImage from '@assets/images/home-background--mobile.svg'; import useThemeStyles from '@hooks/useThemeStyles'; -import defaultPropTypes from './propTypes'; +import type BackgroundImageProps from './types'; -const defaultProps = { - isSmallScreen: false, -}; - -const propTypes = { - /** Is the window width narrow, like on a mobile device */ - isSmallScreen: PropTypes.bool, - - ...defaultPropTypes, -}; -function BackgroundImage(props) { +function BackgroundImage({width, transitionDuration, isSmallScreen = false}: BackgroundImageProps) { const styles = useThemeStyles(); const fadeIn = { from: { @@ -31,16 +20,16 @@ function BackgroundImage(props) { - {props.isSmallScreen ? ( + {isSmallScreen ? ( ) : ( )} @@ -49,7 +38,5 @@ function BackgroundImage(props) { } BackgroundImage.displayName = 'BackgroundImage'; -BackgroundImage.propTypes = propTypes; -BackgroundImage.defaultProps = defaultProps; export default BackgroundImage; diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/propTypes.js b/src/pages/signin/SignInPageLayout/BackgroundImage/propTypes.js deleted file mode 100644 index d966b196b725..000000000000 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/propTypes.js +++ /dev/null @@ -1,14 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - /** pointerEvents property to the SVG element */ - pointerEvents: PropTypes.string.isRequired, - - /** The width of the image. */ - width: PropTypes.number.isRequired, - - /** Transition duration in milliseconds */ - transitionDuration: PropTypes.number, -}; - -export default propTypes; diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/types.ts b/src/pages/signin/SignInPageLayout/BackgroundImage/types.ts new file mode 100644 index 000000000000..72e7a42c0d33 --- /dev/null +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/types.ts @@ -0,0 +1,15 @@ +type BackgroundImageProps = { + /** pointerEvents property to the SVG element */ + pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto'; + + /** The width of the image. */ + width: number; + + /** Is the window width narrow, like on a mobile device */ + isSmallScreen?: boolean; + + /** Transition duration in milliseconds */ + transitionDuration: number; +}; + +export default BackgroundImageProps; diff --git a/src/pages/signin/SignInPageLayout/Footer.js b/src/pages/signin/SignInPageLayout/Footer.tsx similarity index 64% rename from src/pages/signin/SignInPageLayout/Footer.js rename to src/pages/signin/SignInPageLayout/Footer.tsx index 279c18e8fc42..4e2d642e0179 100644 --- a/src/pages/signin/SignInPageLayout/Footer.js +++ b/src/pages/signin/SignInPageLayout/Footer.tsx @@ -1,66 +1,75 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import type {StyleProp, TextStyle} from 'react-native'; import {View} from 'react-native'; -import _ from 'underscore'; import SignInGradient from '@assets/images/home-fade-gradient--mobile.svg'; import Hoverable from '@components/Hoverable'; import * as Expensicons from '@components/Icon/Expensicons'; import ImageSVG from '@components/ImageSVG'; import Text from '@components/Text'; +import type {LinkProps, PressProps} from '@components/TextLink'; import TextLink from '@components/TextLink'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import Licenses from '@pages/signin/Licenses'; import Socials from '@pages/signin/Socials'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import type {SignInPageLayoutProps} from './types'; -const propTypes = { - ...withLocalizePropTypes, - navigateFocus: PropTypes.func.isRequired, +type FooterProps = Pick; + +type FooterColumnRow = (LinkProps | PressProps) & { + translationPath: TranslationPaths; +}; + +type FooterColumnData = { + translationPath: TranslationPaths; + rows: FooterColumnRow[]; }; -const columns = ({navigateFocus}) => [ +const columns = ({navigateFocus = () => {}}: Pick): FooterColumnData[] => [ { translationPath: 'footer.features', rows: [ { - link: CONST.FOOTER.EXPENSE_MANAGEMENT_URL, + href: CONST.FOOTER.EXPENSE_MANAGEMENT_URL, translationPath: 'footer.expenseManagement', }, { - link: CONST.FOOTER.SPEND_MANAGEMENT_URL, + href: CONST.FOOTER.SPEND_MANAGEMENT_URL, translationPath: 'footer.spendManagement', }, { - link: CONST.FOOTER.EXPENSE_REPORTS_URL, + href: CONST.FOOTER.EXPENSE_REPORTS_URL, translationPath: 'footer.expenseReports', }, { - link: CONST.FOOTER.COMPANY_CARD_URL, + href: CONST.FOOTER.COMPANY_CARD_URL, translationPath: 'footer.companyCreditCard', }, { - link: CONST.FOOTER.RECIEPT_SCANNING_URL, + href: CONST.FOOTER.RECIEPT_SCANNING_URL, translationPath: 'footer.receiptScanningApp', }, { - link: CONST.FOOTER.BILL_PAY_URL, + href: CONST.FOOTER.BILL_PAY_URL, translationPath: 'footer.billPay', }, { - link: CONST.FOOTER.INVOICES_URL, + href: CONST.FOOTER.INVOICES_URL, translationPath: 'footer.invoicing', }, { - link: CONST.FOOTER.PAYROLL_URL, + href: CONST.FOOTER.PAYROLL_URL, translationPath: 'footer.payroll', }, { - link: CONST.FOOTER.TRAVEL_URL, + href: CONST.FOOTER.TRAVEL_URL, translationPath: 'footer.travel', }, ], @@ -69,27 +78,27 @@ const columns = ({navigateFocus}) => [ translationPath: 'footer.resources', rows: [ { - link: CONST.FOOTER.EXPENSIFY_APPROVED_URL, + href: CONST.FOOTER.EXPENSIFY_APPROVED_URL, translationPath: 'footer.expensifyApproved', }, { - link: CONST.FOOTER.PRESS_KIT_URL, + href: CONST.FOOTER.PRESS_KIT_URL, translationPath: 'footer.pressKit', }, { - link: CONST.FOOTER.SUPPORT_URL, + href: CONST.FOOTER.SUPPORT_URL, translationPath: 'footer.support', }, { - link: CONST.NEWHELP_URL, + href: CONST.NEWHELP_URL, translationPath: 'footer.expensifyHelp', }, { - link: CONST.FOOTER.COMMUNITY_URL, + href: CONST.FOOTER.COMMUNITY_URL, translationPath: 'footer.community', }, { - link: CONST.FOOTER.PRIVACY_URL, + href: CONST.FOOTER.PRIVACY_URL, translationPath: 'footer.privacy', }, ], @@ -98,23 +107,23 @@ const columns = ({navigateFocus}) => [ translationPath: 'footer.learnMore', rows: [ { - link: CONST.FOOTER.ABOUT_URL, + href: CONST.FOOTER.ABOUT_URL, translationPath: 'footer.aboutExpensify', }, { - link: CONST.FOOTER.BLOG_URL, + href: CONST.FOOTER.BLOG_URL, translationPath: 'footer.blog', }, { - link: CONST.FOOTER.JOBS_URL, + href: CONST.FOOTER.JOBS_URL, translationPath: 'footer.jobs', }, { - link: CONST.FOOTER.ORG_URL, + href: CONST.FOOTER.ORG_URL, translationPath: 'footer.expensifyOrg', }, { - link: CONST.FOOTER.INVESTOR_RELATIONS_URL, + href: CONST.FOOTER.INVESTOR_RELATIONS_URL, translationPath: 'footer.investorRelations', }, ], @@ -134,20 +143,22 @@ const columns = ({navigateFocus}) => [ }, ]; -function Footer(props) { +function Footer({navigateFocus}: FooterProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {isMediumScreenWidth} = useWindowDimensions(); const isVertical = shouldUseNarrowLayout; const imageDirection = isVertical ? styles.flexRow : styles.flexColumn; const imageStyle = isVertical ? styles.pr0 : styles.alignSelfCenter; const columnDirection = isVertical ? styles.flexColumn : styles.flexRow; const pageFooterWrapper = [styles.footerWrapper, imageDirection, imageStyle, isVertical ? styles.pl10 : {}]; const footerColumns = [styles.footerColumnsContainer, columnDirection]; - const footerColumn = isVertical ? [styles.p4] : [styles.p4, props.isMediumScreenWidth ? styles.w50 : styles.w25]; + const footerColumn = isVertical ? [styles.p4] : [styles.p4, isMediumScreenWidth ? styles.w50 : styles.w25]; const footerWrapper = isVertical ? [StyleUtils.getBackgroundColorStyle(theme.signInPage), styles.overflowHidden] : []; - + const getTextLinkStyle: (hovered: boolean) => StyleProp = (hovered) => [styles.footerRow, hovered ? styles.textBlue : {}]; return ( @@ -161,24 +172,32 @@ function Footer(props) { ) : null} - {_.map(columns({navigateFocus: props.navigateFocus}), (column, i) => ( + {columns({navigateFocus}).map((column, i) => ( - {props.translate(column.translationPath)} + {translate(column.translationPath)} - {_.map(column.rows, (row) => ( - + {column.rows.map(({href, onPress, translationPath}) => ( + {(hovered) => ( - - {props.translate(row.translationPath)} - + {onPress ? ( + + {translate(translationPath)} + + ) : ( + + {translate(translationPath)} + + )} )} @@ -214,7 +233,6 @@ function Footer(props) { ); } -Footer.propTypes = propTypes; Footer.displayName = 'Footer'; -export default withLocalize(Footer); +export default Footer; diff --git a/src/pages/signin/SignInPageLayout/SignInHeroCopy.tsx b/src/pages/signin/SignInPageLayout/SignInHeroCopy.tsx new file mode 100644 index 000000000000..baddf70fecca --- /dev/null +++ b/src/pages/signin/SignInPageLayout/SignInHeroCopy.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import variables from '@styles/variables'; +import type {SignInPageLayoutProps} from './types'; + +type SignInHeroCopyProps = Pick; + +function SignInHeroCopy({customHeadline, customHeroBody}: SignInHeroCopyProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {isMediumScreenWidth, isLargeScreenWidth} = useWindowDimensions(); + const {translate} = useLocalize(); + + return ( + + + {customHeadline ?? translate('login.hero.header')} + + {customHeroBody ?? translate('login.hero.body')} + + ); +} + +SignInHeroCopy.displayName = 'SignInHeroCopy'; + +export default SignInHeroCopy; diff --git a/src/pages/signin/SignInHeroImage.js b/src/pages/signin/SignInPageLayout/SignInHeroImage.tsx similarity index 68% rename from src/pages/signin/SignInHeroImage.js rename to src/pages/signin/SignInPageLayout/SignInHeroImage.tsx index 4177f9e9ce6f..538f26e32e93 100644 --- a/src/pages/signin/SignInHeroImage.js +++ b/src/pages/signin/SignInPageLayout/SignInHeroImage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import Lottie from '@components/Lottie'; import LottieAnimations from '@components/LottieAnimations'; @@ -12,23 +12,19 @@ function SignInHeroImage() { const styles = useThemeStyles(); const {isMediumScreenWidth} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - let imageSize; - if (shouldUseNarrowLayout) { - imageSize = { - height: variables.signInHeroImageMobileHeight, - width: variables.signInHeroImageMobileWidth, - }; - } else if (isMediumScreenWidth) { - imageSize = { - height: variables.signInHeroImageTabletHeight, - width: variables.signInHeroImageTabletWidth, - }; - } else { - imageSize = { - height: variables.signInHeroImageDesktopHeight, - width: variables.signInHeroImageDesktopWidth, + const imageSize = useMemo(() => { + if (shouldUseNarrowLayout) { + return { + height: variables.signInHeroImageMobileHeight, + width: variables.signInHeroImageMobileWidth, + }; + } + + return { + height: isMediumScreenWidth ? variables.signInHeroImageTabletHeight : variables.signInHeroImageDesktopHeight, + width: isMediumScreenWidth ? variables.signInHeroImageTabletWidth : variables.signInHeroImageDesktopWidth, }; - } + }, [shouldUseNarrowLayout, isMediumScreenWidth]); const isSplashHidden = useIsSplashHidden(); // Prevents rendering of the Lottie animation until the splash screen is hidden diff --git a/src/pages/signin/SignInPageLayout/SignInPageContent.js b/src/pages/signin/SignInPageLayout/SignInPageContent.tsx similarity index 67% rename from src/pages/signin/SignInPageLayout/SignInPageContent.js rename to src/pages/signin/SignInPageLayout/SignInPageContent.tsx index 2be9a10c6575..770eafa199b7 100755 --- a/src/pages/signin/SignInPageLayout/SignInPageContent.js +++ b/src/pages/signin/SignInPageLayout/SignInPageContent.tsx @@ -1,41 +1,22 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import {withSafeAreaInsets} from 'react-native-safe-area-context'; import ExpensifyWordmark from '@components/ExpensifyWordmark'; import OfflineIndicator from '@components/OfflineIndicator'; import SignInPageForm from '@components/SignInPageForm'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import SignInHeroImage from '@pages/signin/SignInHeroImage'; import variables from '@styles/variables'; +import SignInHeroImage from './SignInHeroImage'; +import type {SignInPageLayoutProps} from './types'; -const propTypes = { +type SignInPageContentProps = Pick & { /** The children to show inside the layout */ - children: PropTypes.node.isRequired, - - /** Welcome text to show in the header of the form, changes depending - * on form type (for example, sign in) */ - welcomeText: PropTypes.string.isRequired, - - /** Welcome header to show in the header of the form, changes depending - * on form type (for example. sign in) and small vs large screens */ - welcomeHeader: PropTypes.string.isRequired, - - /** Whether to show welcome text on a particular page */ - shouldShowWelcomeText: PropTypes.bool.isRequired, - - /** Whether to show welcome header on a particular page */ - shouldShowWelcomeHeader: PropTypes.bool.isRequired, - - ...withLocalizePropTypes, + children?: React.ReactNode; }; -function SignInPageContent(props) { +function SignInPageContent({shouldShowWelcomeHeader, welcomeHeader, welcomeText, shouldShowWelcomeText, children}: SignInPageContentProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -51,25 +32,25 @@ function SignInPageContent(props) { - {props.shouldShowWelcomeHeader && props.welcomeHeader ? ( + {shouldShowWelcomeHeader && welcomeHeader ? ( - {props.welcomeHeader} + {welcomeHeader} ) : null} - {props.shouldShowWelcomeText && props.welcomeText ? ( - {props.welcomeText} + {shouldShowWelcomeText && welcomeText ? ( + {welcomeText} ) : null} - {props.children} + {children} @@ -85,7 +66,6 @@ function SignInPageContent(props) { ); } -SignInPageContent.propTypes = propTypes; SignInPageContent.displayName = 'SignInPageContent'; -export default compose(withLocalize, withSafeAreaInsets)(SignInPageContent); +export default SignInPageContent; diff --git a/src/pages/signin/SignInPageLayout/SignInPageHero.tsx b/src/pages/signin/SignInPageLayout/SignInPageHero.tsx new file mode 100644 index 000000000000..e27aaec6bfd6 --- /dev/null +++ b/src/pages/signin/SignInPageLayout/SignInPageHero.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import {View} from 'react-native'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import variables from '@styles/variables'; +import SignInHeroCopy from './SignInHeroCopy'; +import SignInHeroImage from './SignInHeroImage'; +import type {SignInPageLayoutProps} from './types'; + +type SignInPageHeroProps = Pick; + +function SignInPageHero({customHeadline, customHeroBody}: SignInPageHeroProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth, windowHeight} = useWindowDimensions(); + return ( + + + + + + + ); +} + +SignInPageHero.displayName = 'SignInPageHero'; + +export default SignInPageHero; diff --git a/src/pages/signin/SignInPageLayout/index.js b/src/pages/signin/SignInPageLayout/index.tsx similarity index 58% rename from src/pages/signin/SignInPageLayout/index.js rename to src/pages/signin/SignInPageLayout/index.tsx index 797ab6bd7e97..b65da7eba0a5 100644 --- a/src/pages/signin/SignInPageLayout/index.js +++ b/src/pages/signin/SignInPageLayout/index.tsx @@ -1,79 +1,58 @@ -import PropTypes from 'prop-types'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect, useImperativeHandle, useMemo, useRef} from 'react'; import {ScrollView, View} from 'react-native'; -import {withSafeAreaInsets} from 'react-native-safe-area-context'; import SignInGradient from '@assets/images/home-fade-gradient.svg'; import ImageSVG from '@components/ImageSVG'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; -import SignInPageHero from '@pages/signin/SignInPageHero'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import BackgroundImage from './BackgroundImage'; import Footer from './Footer'; import SignInPageContent from './SignInPageContent'; +import SignInPageHero from './SignInPageHero'; import scrollViewContentContainerStyles from './signInPageStyles'; - -const propTypes = { - /** The children to show inside the layout */ - children: PropTypes.node.isRequired, - - /** Welcome text to show in the header of the form, changes depending - * on form type (for example, sign in) */ - welcomeText: PropTypes.string.isRequired, - - /** Welcome header to show in the header of the form, changes depending - * on form type (for example, sign in) and small vs large screens */ - welcomeHeader: PropTypes.string.isRequired, - - /** Whether to show welcome text on a particular page */ - shouldShowWelcomeText: PropTypes.bool.isRequired, - - /** 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]), - - /** Override the green headline copy */ - customHeadline: PropTypes.string, - - /** Override the smaller hero body copy below the headline */ - customHeroBody: PropTypes.string, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - innerRef: () => {}, - customHeadline: '', - customHeroBody: '', -}; - -function SignInPageLayout(props) { +import type {SignInPageLayoutProps, SignInPageLayoutRef} from './types'; + +function SignInPageLayout( + { + customHeadline, + customHeroBody, + shouldShowWelcomeHeader = false, + welcomeHeader, + welcomeText = '', + shouldShowWelcomeText = false, + navigateFocus = () => {}, + children, + }: SignInPageLayoutProps, + ref: ForwardedRef, +) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const scrollViewRef = useRef(); - const prevPreferredLocale = usePrevious(props.preferredLocale); - let containerStyles = [styles.flex1, styles.signInPageInner]; - let contentContainerStyles = [styles.flex1, styles.flexRow]; - const {windowHeight} = useWindowDimensions(); + const {preferredLocale} = useLocalize(); + const {top: topInsets, bottom: bottomInsets} = useSafeAreaInsets(); + const scrollViewRef = useRef(null); + const prevPreferredLocale = usePrevious(preferredLocale); + const {windowHeight, isMediumScreenWidth, isLargeScreenWidth} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - // To scroll on both mobile and web, we need to set the container height manually - const containerHeight = windowHeight - props.insets.top - props.insets.bottom; + const {containerStyles, contentContainerStyles} = useMemo( + () => ({ + containerStyles: shouldUseNarrowLayout ? [styles.flex1] : [styles.flex1, styles.signInPageInner], + contentContainerStyles: [styles.flex1, shouldUseNarrowLayout ? styles.flexColumn : styles.flexRow], + }), + [shouldUseNarrowLayout, styles], + ); - if (shouldUseNarrowLayout) { - containerStyles = [styles.flex1]; - contentContainerStyles = [styles.flex1, styles.flexColumn]; - } + // To scroll on both mobile and web, we need to set the container height manually + const containerHeight = windowHeight - topInsets - bottomInsets; const scrollPageToTop = (animated = false) => { if (!scrollViewRef.current) { @@ -82,17 +61,17 @@ function SignInPageLayout(props) { scrollViewRef.current.scrollTo({y: 0, animated}); }; - useImperativeHandle(props.innerRef, () => ({ + useImperativeHandle(ref, () => ({ scrollPageToTop, })); useEffect(() => { - if (prevPreferredLocale !== props.preferredLocale) { + if (prevPreferredLocale !== preferredLocale) { return; } scrollPageToTop(); - }, [props.welcomeHeader, props.welcomeText, prevPreferredLocale, props.preferredLocale]); + }, [welcomeHeader, welcomeText, prevPreferredLocale, preferredLocale]); const scrollViewStyles = useMemo(() => scrollViewContentContainerStyles(styles), [styles]); @@ -108,12 +87,12 @@ function SignInPageLayout(props) { contentContainerStyle={[styles.flex1]} > - {props.children} + {children} -