[NEW] Flow to resend email confirmation from mobile (#3439)

* [NEW] Flow to resend email confirmation from mobile

* Hooks

* theme

* minor tweak

* refactor: improving withTheme ts

* minor tweaks

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>
This commit is contained in:
Reinaldo Neto 2021-10-20 13:42:44 -03:00 committed by GitHub
parent 42c69f0c06
commit 15ccb73f37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 147 additions and 23 deletions

View File

@ -1,5 +1,5 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { StyleProp, StyleSheet, Text, TextInputProps, TextStyle, View, ViewStyle } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import sharedStyles from '../views/Styles';
@ -50,23 +50,21 @@ const styles = StyleSheet.create({
}
});
interface IRCTextInputProps {
label: string;
error: {
interface IRCTextInputProps extends TextInputProps {
label?: string;
error?: {
error: any;
reason: any;
};
loading: boolean;
secureTextEntry: boolean;
containerStyle: any;
inputStyle: object;
inputRef: any;
testID: string;
iconLeft: string;
iconRight: string;
placeholder: string;
left: JSX.Element;
onIconRightPress(): void;
loading?: boolean;
containerStyle?: StyleProp<ViewStyle>;
inputStyle?: TextStyle;
inputRef?: React.Ref<unknown>;
testID?: string;
iconLeft?: string;
iconRight?: string;
left?: JSX.Element;
onIconRightPress?(): void;
theme: string;
}
@ -152,7 +150,7 @@ export default class RCTextInput extends React.PureComponent<IRCTextInputProps,
contentDescription={null}
// @ts-ignore
accessibilityLabel={null}
style={[styles.label, { color: themes[theme].titleText }, error.error && { color: dangerColor }]}>
style={[styles.label, { color: themes[theme].titleText }, error?.error && { color: dangerColor }]}>
{label}
</Text>
) : null}
@ -168,7 +166,7 @@ export default class RCTextInput extends React.PureComponent<IRCTextInputProps,
borderColor: themes[theme].separatorColor,
color: themes[theme].titleText
},
error.error && {
error?.error && {
color: dangerColor,
borderColor: dangerColor
},

View File

@ -780,5 +780,7 @@
"Content": "Content",
"Sharing": "Sharing",
"No_canned_responses": "No canned responses",
"Send_email_confirmation": "Send email confirmation",
"sending_email_confirmation": "sending email confirmation",
"Enable_Message_Parser": "Enable Message Parser"
}

View File

@ -679,5 +679,7 @@
"Use": "Use",
"Shortcut": "Atalho",
"Content": "Conteúdo",
"No_canned_responses": "Não há respostas predefinidas"
"No_canned_responses": "Não há respostas predefinidas",
"Send_email_confirmation": "Enviar email de confirmação",
"sending_email_confirmation": "enviando email de confirmação"
}

View File

@ -5,6 +5,7 @@ import { Q } from '@nozbe/watermelondb';
import AsyncStorage from '@react-native-community/async-storage';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import RNFetchBlob from 'rn-fetch-blob';
import isEmpty from 'lodash/isEmpty';
import defaultSettings from '../constants/settings';
import log from '../utils/log';
@ -530,6 +531,10 @@ const RocketChat = {
return this.post('users.forgotPassword', { email }, false);
},
sendConfirmationEmail(email) {
return this.methodCallWrapper('sendConfirmationEmail', email);
},
loginTOTP(params, loginEmailPassword, isFromWebView = false) {
return new Promise(async (resolve, reject) => {
try {
@ -1060,8 +1065,12 @@ const RocketChat = {
},
methodCallWrapper(method, ...params) {
const { API_Use_REST_For_DDP_Calls } = reduxStore.getState().settings;
const { user } = reduxStore.getState().login;
if (API_Use_REST_For_DDP_Calls) {
return this.post(`method.call/${method}`, { message: EJSON.stringify({ method, params }) });
const url = isEmpty(user) ? 'method.callAnon' : 'method.call';
return this.post(`${url}/${method}`, {
message: EJSON.stringify({ method, params })
});
}
const parsedParams = params.map(param => {
if (param instanceof Date) {

View File

@ -10,6 +10,7 @@ import NewServerView from '../views/NewServerView';
import WorkspaceView from '../views/WorkspaceView';
import LoginView from '../views/LoginView';
import ForgotPasswordView from '../views/ForgotPasswordView';
import SendEmailConfirmationView from '../views/SendEmailConfirmationView';
import RegisterView from '../views/RegisterView';
import LegalView from '../views/LegalView';
import AuthenticationWebView from '../views/AuthenticationWebView';
@ -25,6 +26,11 @@ const _OutsideStack = () => {
<Outside.Screen name='WorkspaceView' component={WorkspaceView} options={WorkspaceView.navigationOptions} />
<Outside.Screen name='LoginView' component={LoginView} options={LoginView.navigationOptions} />
<Outside.Screen name='ForgotPasswordView' component={ForgotPasswordView} options={ForgotPasswordView.navigationOptions} />
<Outside.Screen
name='SendEmailConfirmationView'
component={SendEmailConfirmationView}
options={SendEmailConfirmationView.navigationOptions}
/>
<Outside.Screen name='RegisterView' component={RegisterView} options={RegisterView.navigationOptions} />
<Outside.Screen name='LegalView' component={LegalView} options={LegalView.navigationOptions} />
</Outside.Navigator>

View File

@ -3,14 +3,14 @@ import hoistNonReactStatics from 'hoist-non-react-statics';
interface IThemeContextProps {
theme: string;
themePreferences: {
themePreferences?: {
currentTheme: 'automatic' | 'light';
darkLevel: string;
};
setTheme: (newTheme?: {}) => void;
setTheme?: (newTheme?: {}) => void;
}
export const ThemeContext = React.createContext<Partial<IThemeContextProps>>({ theme: 'light' });
export const ThemeContext = React.createContext<IThemeContextProps>({ theme: 'light' });
export function withTheme(Component: any) {
const ThemedComponent = (props: any) => (

View File

@ -11,6 +11,9 @@ export default {
FP_FORGOT_PASSWORD: 'fp_forgot_password',
FP_FORGOT_PASSWORD_F: 'fp_forgot_password_f',
// SEND EMAIL CONFIRMATION VIEW
SEC_SEND_EMAIL_CONFIRMATION: 'sec_send_email_confirmation',
// REGISTER VIEW
REGISTER_DEFAULT_SIGN_UP: 'register_default_sign_up',
REGISTER_DEFAULT_SIGN_UP_F: 'register_default_sign_up_f',

View File

@ -81,7 +81,11 @@ class LoginView extends React.Component {
UNSAFE_componentWillReceiveProps(nextProps) {
const { error } = this.props;
if (nextProps.failure && !dequal(error, nextProps.error)) {
Alert.alert(I18n.t('Oops'), I18n.t('Login_error'));
if (nextProps.error.error === 'error-invalid-email') {
this.resendEmailConfirmation();
} else {
Alert.alert(I18n.t('Oops'), I18n.t('Login_error'));
}
}
}
@ -105,6 +109,12 @@ class LoginView extends React.Component {
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();

View File

@ -0,0 +1,94 @@
import React, { useEffect, useState } from 'react';
import { StackNavigationProp } from '@react-navigation/stack';
import TextInput from '../containers/TextInput';
import Button from '../containers/Button';
import { showErrorAlert } from '../utils/info';
import isValidEmail from '../utils/isValidEmail';
import I18n from '../i18n';
import RocketChat from '../lib/rocketchat';
import { useTheme } from '../theme';
import FormContainer, { FormContainerInner } from '../containers/FormContainer';
import log, { events, logEvent } from '../utils/log';
import sharedStyles from './Styles';
interface ISendEmailConfirmationView {
navigation: StackNavigationProp<any, 'SendEmailConfirmationView'>;
route: {
params: {
user?: string;
};
};
}
const SendEmailConfirmationView = ({ navigation, route }: ISendEmailConfirmationView): JSX.Element => {
const [email, setEmail] = useState('');
const [invalidEmail, setInvalidEmail] = useState(true);
const [isFetching, setIsFetching] = useState(false);
const { theme } = useTheme();
const validate = (val: string) => {
const isInvalidEmail = !isValidEmail(val);
setEmail(val);
setInvalidEmail(isInvalidEmail);
};
const resendConfirmationEmail = async () => {
logEvent(events.SEC_SEND_EMAIL_CONFIRMATION);
if (invalidEmail || !email) {
return;
}
try {
setIsFetching(true);
const result = await RocketChat.sendConfirmationEmail(email);
if (result.success) {
navigation.pop();
showErrorAlert(I18n.t('Verify_email_desc'));
}
} catch (e: any) {
log(e);
const msg = e?.data?.error || I18n.t('There_was_an_error_while_action', { action: I18n.t('sending_email_confirmation') });
showErrorAlert(msg, I18n.t('Alert'));
}
setIsFetching(false);
};
useEffect(() => {
navigation.setOptions({
title: 'Rocket.Chat'
});
if (route.params?.user) {
validate(route.params.user);
}
}, []);
return (
<FormContainer theme={theme} testID='send-email-confirmation-view'>
<FormContainerInner>
<TextInput
autoFocus
placeholder={I18n.t('Email')}
keyboardType='email-address'
returnKeyType='send'
onChangeText={(email: string) => validate(email)}
onSubmitEditing={resendConfirmationEmail}
testID='send-email-confirmation-view-email'
containerStyle={sharedStyles.inputLastChild}
theme={theme}
value={email}
/>
<Button
title={I18n.t('Send_email_confirmation')}
type='primary'
onPress={resendConfirmationEmail}
testID='send-email-confirmation-view-submit'
loading={isFetching}
disabled={invalidEmail}
theme={theme}
/>
</FormContainerInner>
</FormContainer>
);
};
export default SendEmailConfirmationView;