From c0660db06d2d3ac8ac45bcae0ab32cbf96cd2670 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:13:42 -0300 Subject: [PATCH 01/14] chore: migrate AuthenticationWebView to hooks (#5054) * chore: migrate AuthenticationWebView to hooks * minor tweak * remove navigation, tweak at useRoute --------- Co-authored-by: GleidsonDaniel --- app/lib/methods/helpers/debounce.ts | 6 +- app/stacks/OutsideStack.tsx | 6 +- app/views/AuthenticationWebView.tsx | 181 ++++++++++------------------ 3 files changed, 71 insertions(+), 122 deletions(-) diff --git a/app/lib/methods/helpers/debounce.ts b/app/lib/methods/helpers/debounce.ts index 5061e322b..4a58b6d76 100644 --- a/app/lib/methods/helpers/debounce.ts +++ b/app/lib/methods/helpers/debounce.ts @@ -1,4 +1,4 @@ -import { useDebouncedCallback } from 'use-debounce'; +import { useDebouncedCallback, Options } from 'use-debounce'; export function debounce(func: Function, wait?: number, immediate?: boolean) { let timeout: ReturnType | null; @@ -24,6 +24,6 @@ export function debounce(func: Function, wait?: number, immediate?: boolean) { return _debounce; } -export function useDebounce(func: (...args: any) => any, wait?: number): (...args: any[]) => void { - return useDebouncedCallback(func, wait || 1000); +export function useDebounce(func: (...args: any) => any, wait?: number, options?: Options): (...args: any[]) => void { + return useDebouncedCallback(func, wait || 1000, options); } diff --git a/app/stacks/OutsideStack.tsx b/app/stacks/OutsideStack.tsx index ec19c2319..40a3c9cb1 100644 --- a/app/stacks/OutsideStack.tsx +++ b/app/stacks/OutsideStack.tsx @@ -50,11 +50,7 @@ const OutsideStackModal = () => { screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...ModalAnimation, presentation: 'transparentModal' }} > - + ); }; diff --git a/app/views/AuthenticationWebView.tsx b/app/views/AuthenticationWebView.tsx index 894c524b9..4874f13a1 100644 --- a/app/views/AuthenticationWebView.tsx +++ b/app/views/AuthenticationWebView.tsx @@ -1,20 +1,20 @@ -import React from 'react'; -import { WebView, WebViewNavigation } from 'react-native-webview'; -import { connect } from 'react-redux'; -import parse from 'url-parse'; -import { StackNavigationProp } from '@react-navigation/stack'; -import { WebViewMessage } from 'react-native-webview/lib/WebViewTypes'; import { RouteProp } from '@react-navigation/core'; +import { useNavigation, useRoute } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import React, { useLayoutEffect, useState } from 'react'; +import { WebView, WebViewNavigation } from 'react-native-webview'; +import { WebViewMessage } from 'react-native-webview/lib/WebViewTypes'; +import parse from 'url-parse'; -import { OutsideModalParamList } from '../stacks/types'; -import StatusBar from '../containers/StatusBar'; import ActivityIndicator from '../containers/ActivityIndicator'; -import { TSupportedThemes, withTheme } from '../theme'; -import { userAgent } from '../lib/constants'; -import { debounce } from '../lib/methods/helpers'; import * as HeaderButton from '../containers/HeaderButton'; +import StatusBar from '../containers/StatusBar'; +import { ICredentials } from '../definitions'; +import { userAgent } from '../lib/constants'; +import { useAppSelector } from '../lib/hooks'; +import { useDebounce } from '../lib/methods/helpers'; import { Services } from '../lib/services'; -import { IApplicationState, ICredentials } from '../definitions'; +import { OutsideModalParamList } from '../stacks/types'; // iframe uses a postMessage to send the token to the client // We'll handle this sending the token to the hash of the window.location @@ -40,95 +40,56 @@ window.addEventListener('popstate', function() { }); `; -interface INavigationOption { - navigation: StackNavigationProp; - route: RouteProp; -} +const AuthenticationWebView = () => { + const [logging, setLogging] = useState(false); + const [loading, setLoading] = useState(false); -interface IAuthenticationWebView extends INavigationOption { - server: string; - Accounts_Iframe_api_url: string; - Accounts_Iframe_api_method: string; - theme: TSupportedThemes; -} + const navigation = useNavigation>(); + const { + params: { authType, url, ssoToken } + } = useRoute>(); -interface IState { - logging: boolean; - loading: boolean; -} + const { Accounts_Iframe_api_method, Accounts_Iframe_api_url, server } = useAppSelector(state => ({ + server: state.server.server, + Accounts_Iframe_api_url: state.settings.Accounts_Iframe_api_url as string, + Accounts_Iframe_api_method: state.settings.Accounts_Iframe_api_method as string + })); -class AuthenticationWebView extends React.PureComponent { - private oauthRedirectRegex: RegExp; - private iframeRedirectRegex: RegExp; + const oauthRedirectRegex = new RegExp(`(?=.*(${server}))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g'); + const iframeRedirectRegex = new RegExp(`(?=.*(${server}))(?=.*(event|loginToken|token))`, 'g'); - static navigationOptions = ({ route, navigation }: INavigationOption) => { - const { authType } = route.params; - return { - headerLeft: () => , - title: ['saml', 'cas', 'iframe'].includes(authType) ? 'SSO' : 'OAuth' - }; - }; + // Force 3s delay so the server has time to evaluate the token + const debouncedLogin = useDebounce((params: ICredentials) => login(params), 3000); - constructor(props: IAuthenticationWebView) { - super(props); - this.state = { - logging: false, - loading: false - }; - this.oauthRedirectRegex = new RegExp(`(?=.*(${props.server}))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g'); - this.iframeRedirectRegex = new RegExp(`(?=.*(${props.server}))(?=.*(event|loginToken|token))`, 'g'); - } - - componentWillUnmount() { - if (this.debouncedLogin && this.debouncedLogin.stop) { - this.debouncedLogin.stop(); - } - } - - dismiss = () => { - const { navigation } = this.props; - navigation.pop(); - }; - - login = (params: ICredentials) => { - const { logging } = this.state; + const login = (params: ICredentials) => { if (logging) { return; } - - this.setState({ logging: true }); - + setLogging(true); try { Services.loginOAuthOrSso(params); } catch (e) { console.warn(e); } - this.setState({ logging: false }); - this.dismiss(); + setLogging(false); + navigation.pop(); }; - // Force 3s delay so the server has time to evaluate the token - debouncedLogin = debounce((params: ICredentials) => this.login(params), 3000); - - tryLogin = debounce( + const tryLogin = useDebounce( async () => { - const { Accounts_Iframe_api_url, Accounts_Iframe_api_method } = this.props; const data = await fetch(Accounts_Iframe_api_url, { method: Accounts_Iframe_api_method }).then(response => response.json()); const resume = data?.login || data?.loginToken; if (resume) { - this.login({ resume }); + login({ resume }); } }, 3000, - true + { leading: true } ); - onNavigationStateChange = (webViewState: WebViewNavigation | WebViewMessage) => { + const onNavigationStateChange = (webViewState: WebViewNavigation | WebViewMessage) => { const url = decodeURIComponent(webViewState.url); - const { route } = this.props; - const { authType } = route.params; if (authType === 'saml' || authType === 'cas') { - const { ssoToken } = route.params; const parsedUrl = parse(url, true); // ticket -> cas / validate & saml_idp_credentialToken -> saml if (parsedUrl.pathname?.includes('validate') || parsedUrl.query?.ticket || parsedUrl.query?.saml_idp_credentialToken) { @@ -140,28 +101,28 @@ class AuthenticationWebView extends React.PureComponent - - this.onNavigationStateChange(nativeEvent)} - onNavigationStateChange={this.onNavigationStateChange} - injectedJavaScript={isIframe ? injectedJavaScript : undefined} - onLoadStart={() => { - this.setState({ loading: true }); - }} - onLoadEnd={() => { - this.setState({ loading: false }); - }} - /> - {loading ? : null} - - ); - } -} + useLayoutEffect(() => { + navigation.setOptions({ + headerLeft: () => , + title: ['saml', 'cas', 'iframe'].includes(authType) ? 'SSO' : 'OAuth' + }); + }, [authType, navigation]); -const mapStateToProps = (state: IApplicationState) => ({ - server: state.server.server, - Accounts_Iframe_api_url: state.settings.Accounts_Iframe_api_url as string, - Accounts_Iframe_api_method: state.settings.Accounts_Iframe_api_method as string -}); + return ( + <> + + onNavigationStateChange(nativeEvent)} + onNavigationStateChange={onNavigationStateChange} + injectedJavaScript={isIframe ? injectedJavaScript : undefined} + onLoadStart={() => setLoading(true)} + onLoadEnd={() => setLoading(false)} + /> + {loading ? : null} + + ); +}; -export default connect(mapStateToProps)(withTheme(AuthenticationWebView)); +export default AuthenticationWebView; From 3f2e5ced197e9a56f70e106e72671d54213e4c16 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:23:12 -0300 Subject: [PATCH 02/14] chore: migrate LoginView to hooks (#5092) * chore: migrate LoginView to hooks * minor tweak * minor tweak * fix ref --------- Co-authored-by: GleidsonDaniel --- .../TextInput/ControlledFormTextInput.tsx | 6 +- app/views/LoginView.tsx | 264 ------------------ app/views/LoginView/UserForm.tsx | 171 ++++++++++++ app/views/LoginView/index.tsx | 40 +++ app/views/LoginView/styles.ts | 36 +++ 5 files changed, 251 insertions(+), 266 deletions(-) delete mode 100644 app/views/LoginView.tsx create mode 100644 app/views/LoginView/UserForm.tsx create mode 100644 app/views/LoginView/index.tsx create mode 100644 app/views/LoginView/styles.ts diff --git a/app/containers/TextInput/ControlledFormTextInput.tsx b/app/containers/TextInput/ControlledFormTextInput.tsx index 5b69b0e20..d2aa1d446 100644 --- a/app/containers/TextInput/ControlledFormTextInput.tsx +++ b/app/containers/TextInput/ControlledFormTextInput.tsx @@ -3,7 +3,7 @@ import { Control, Controller } from 'react-hook-form'; import { FormTextInput, IRCTextInputProps } from './FormTextInput'; -interface IControlledFormTextInputProps extends IRCTextInputProps { +interface IControlledFormTextInputProps extends Omit { control: Control; name: string; } @@ -12,6 +12,8 @@ export const ControlledFormTextInput = ({ control, name, ...props }: IControlled } + render={({ field: { onChange, value, ref } }) => ( + + )} /> ); diff --git a/app/views/LoginView.tsx b/app/views/LoginView.tsx deleted file mode 100644 index b43830f73..000000000 --- a/app/views/LoginView.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { dequal } from 'dequal'; -import React from 'react'; -import { Alert, Keyboard, StyleSheet, Text, View, TextInput as RNTextInput } from 'react-native'; -import { connect } from 'react-redux'; - -import { loginRequest } from '../actions/login'; -import { themes } from '../lib/constants'; -import Button from '../containers/Button'; -import FormContainer, { FormContainerInner } from '../containers/FormContainer'; -import * as HeaderButton from '../containers/HeaderButton'; -import LoginServices from '../containers/LoginServices'; -import { FormTextInput } from '../containers/TextInput'; -import { IApplicationState, IBaseScreen } from '../definitions'; -import I18n from '../i18n'; -import { OutsideParamList } from '../stacks/types'; -import { withTheme } from '../theme'; -import sharedStyles from './Styles'; -import UGCRules from '../containers/UserGeneratedContentRules'; - -const styles = StyleSheet.create({ - registerDisabled: { - ...sharedStyles.textRegular, - ...sharedStyles.textAlignCenter, - fontSize: 16 - }, - title: { - ...sharedStyles.textBold, - fontSize: 22 - }, - inputContainer: { - marginVertical: 16 - }, - bottomContainer: { - flexDirection: 'column', - alignItems: 'center' - }, - bottomContainerText: { - ...sharedStyles.textRegular, - fontSize: 13 - }, - bottomContainerTextBold: { - ...sharedStyles.textSemibold, - fontSize: 13 - }, - loginButton: { - marginTop: 16 - }, - ugcContainer: { - marginTop: 32 - } -}); - -interface ILoginViewProps extends IBaseScreen { - Site_Name: string; - Accounts_RegistrationForm: string; - Accounts_RegistrationForm_LinkReplacementText: string; - Accounts_EmailOrUsernamePlaceholder: string; - Accounts_PasswordPlaceholder: string; - Accounts_PasswordReset: boolean; - Accounts_ShowFormLogin: boolean; - isFetching: boolean; - error: { - error: string; - }; - failure: boolean; - loginRequest: Function; - inviteLinkToken: string; -} - -interface ILoginViewState { - user: string; - password: string; -} - -class LoginView extends React.Component { - private passwordInput: RNTextInput | null | undefined; - - static navigationOptions = ({ route, navigation }: ILoginViewProps) => ({ - title: route?.params?.title ?? 'Rocket.Chat', - headerRight: () => - }); - - constructor(props: ILoginViewProps) { - super(props); - this.state = { - user: props.route.params?.username ?? '', - password: '' - }; - } - - UNSAFE_componentWillReceiveProps(nextProps: ILoginViewProps) { - const { error } = this.props; - if (nextProps.failure && !dequal(error, nextProps.error)) { - if (nextProps.error?.error === 'error-invalid-email') { - this.resendEmailConfirmation(); - } else { - Alert.alert(I18n.t('Oops'), I18n.t('Login_error')); - } - } - } - - get showRegistrationButton() { - const { Accounts_RegistrationForm, inviteLinkToken } = this.props; - return Accounts_RegistrationForm === 'Public' || (Accounts_RegistrationForm === 'Secret URL' && inviteLinkToken?.length); - } - - login = () => { - const { navigation, Site_Name } = this.props; - navigation.navigate('LoginView', { title: Site_Name }); - }; - - register = () => { - const { navigation, Site_Name } = this.props; - navigation.navigate('RegisterView', { title: Site_Name }); - }; - - forgotPassword = () => { - const { navigation, Site_Name } = this.props; - navigation.navigate('ForgotPasswordView', { title: Site_Name }); - }; - - resendEmailConfirmation = () => { - const { user } = this.state; - const { navigation } = this.props; - navigation.navigate('SendEmailConfirmationView', { user }); - }; - - valid = () => { - const { user, password } = this.state; - return user.trim() && password.trim(); - }; - - submit = () => { - if (!this.valid()) { - return; - } - - const { user, password } = this.state; - const { dispatch } = this.props; - Keyboard.dismiss(); - dispatch(loginRequest({ user, password })); - }; - - renderUserForm = () => { - const { user } = this.state; - const { - Accounts_EmailOrUsernamePlaceholder, - Accounts_PasswordPlaceholder, - Accounts_PasswordReset, - Accounts_RegistrationForm_LinkReplacementText, - isFetching, - theme, - Accounts_ShowFormLogin - } = this.props; - - if (!Accounts_ShowFormLogin) { - return null; - } - - return ( - <> - {I18n.t('Login')} - this.setState({ user: value })} - onSubmitEditing={() => { - this.passwordInput?.focus(); - }} - testID='login-view-email' - textContentType='username' - autoComplete='username' - value={user} - /> - { - this.passwordInput = e; - }} - placeholder={Accounts_PasswordPlaceholder || I18n.t('Password')} - returnKeyType='send' - secureTextEntry - onSubmitEditing={this.submit} - onChangeText={(value: string) => this.setState({ password: value })} - testID='login-view-password' - textContentType='password' - autoComplete='password' - /> -