import React from 'react'; import PropTypes from 'prop-types'; import { Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity, View, Alert } from 'react-native'; import { connect } from 'react-redux'; import SafeAreaView from 'react-native-safe-area-view'; import * as FileSystem from 'expo-file-system'; import DocumentPicker from 'react-native-document-picker'; import ActionSheet from 'react-native-action-sheet'; import isEqual from 'deep-equal'; import { serverRequest } from '../actions/server'; import sharedStyles from './Styles'; import scrollPersistTaps from '../utils/scrollPersistTaps'; import Button from '../containers/Button'; import TextInput from '../containers/TextInput'; import I18n from '../i18n'; import { verticalScale, moderateScale } from '../utils/scaling'; import KeyboardView from '../presentation/KeyboardView'; import { isIOS, isNotch } from '../utils/deviceInfo'; import { CustomIcon } from '../lib/Icons'; import StatusBar from '../containers/StatusBar'; import { COLOR_PRIMARY } from '../constants/colors'; import log from '../utils/log'; import { animateNextTransition } from '../utils/layoutAnimation'; const styles = StyleSheet.create({ image: { alignSelf: 'center', marginVertical: verticalScale(20), width: 210, height: 171 }, title: { ...sharedStyles.textBold, ...sharedStyles.textColorNormal, fontSize: moderateScale(22), letterSpacing: 0, alignSelf: 'center' }, inputContainer: { marginTop: 25, marginBottom: 15 }, backButton: { position: 'absolute', paddingHorizontal: 9, left: 15 }, certificatePicker: { flex: 1, marginTop: 40, alignItems: 'center', justifyContent: 'center' }, chooseCertificateTitle: { fontSize: 15, ...sharedStyles.textRegular, ...sharedStyles.textColorDescription }, chooseCertificate: { fontSize: 15, ...sharedStyles.textSemibold, ...sharedStyles.textColorHeaderBack } }); const defaultServer = 'https://open.rocket.chat'; class NewServerView extends React.Component { static navigationOptions = () => ({ header: null }) static propTypes = { navigation: PropTypes.object, server: PropTypes.string, connecting: PropTypes.bool.isRequired, connectServer: PropTypes.func.isRequired } constructor(props) { super(props); const server = props.navigation.getParam('server'); // Cancel this.options = [I18n.t('Cancel')]; this.CANCEL_INDEX = 0; // Delete this.options.push(I18n.t('Delete')); this.DELETE_INDEX = 1; this.state = { text: server || '', autoFocus: !server, certificate: null }; } componentDidMount() { const { text } = this.state; const { connectServer } = this.props; if (text) { connectServer(text); } } shouldComponentUpdate(nextProps, nextState) { const { text, certificate } = this.state; const { connecting } = this.props; if (nextState.text !== text) { return true; } if (!isEqual(nextState.certificate, certificate)) { return true; } if (nextProps.connecting !== connecting) { return true; } return false; } onChangeText = (text) => { this.setState({ text }); } submit = async() => { const { text, certificate } = this.state; const { connectServer } = this.props; let cert = null; if (certificate) { const certificatePath = `${ FileSystem.documentDirectory }/${ certificate.name }`; try { await FileSystem.copyAsync({ from: certificate.path, to: certificatePath }); } catch (e) { log(e); } cert = { path: this.uriToPath(certificatePath), // file:// isn't allowed by obj-C password: certificate.password }; } if (text) { Keyboard.dismiss(); connectServer(this.completeUrl(text), cert); } } chooseCertificate = async() => { try { const res = await DocumentPicker.pick({ type: ['com.rsa.pkcs-12'] }); const { uri: path, name } = res; Alert.prompt( I18n.t('Certificate_password'), I18n.t('Whats_the_password_for_your_certificate'), [ { text: 'OK', onPress: password => this.saveCertificate({ path, name, password }) } ], 'secure-text', ); } catch (e) { if (!DocumentPicker.isCancel(e)) { log(e); } } } completeUrl = (url) => { url = url && url.trim(); if (/^(\w|[0-9-_]){3,}$/.test(url) && /^(htt(ps?)?)|(loca((l)?|(lh)?|(lho)?|(lhos)?|(lhost:?\d*)?)$)/.test(url) === false) { url = `${ url }.rocket.chat`; } if (/^(https?:\/\/)?(((\w|[0-9-_])+(\.(\w|[0-9-_])+)+)|localhost)(:\d+)?$/.test(url)) { if (/^localhost(:\d+)?/.test(url)) { url = `http://${ url }`; } else if (/^https?:\/\//.test(url) === false) { url = `https://${ url }`; } } return url.replace(/\/+$/, ''); } uriToPath = uri => uri.replace('file://', ''); saveCertificate = (certificate) => { animateNextTransition(); this.setState({ certificate }); } handleDelete = () => this.setState({ certificate: null }); // We not need delete file from DocumentPicker because it is a temp file showActionSheet = () => { ActionSheet.showActionSheetWithOptions({ options: this.options, cancelButtonIndex: this.CANCEL_INDEX, destructiveButtonIndex: this.DELETE_INDEX }, (actionIndex) => { if (actionIndex === this.DELETE_INDEX) { this.handleDelete(); } }); } renderBack = () => { const { navigation } = this.props; let top = 15; if (isIOS) { top = isNotch ? 45 : 30; } return ( <TouchableOpacity style={[styles.backButton, { top }]} onPress={() => navigation.pop()} > <CustomIcon name='back' size={30} color={COLOR_PRIMARY} /> </TouchableOpacity> ); } renderCertificatePicker = () => { const { certificate } = this.state; return ( <View style={styles.certificatePicker}> <Text style={styles.chooseCertificateTitle}>{certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')}</Text> <TouchableOpacity onPress={certificate ? this.showActionSheet : this.chooseCertificate} testID='new-server-choose-certificate'> <Text style={styles.chooseCertificate}>{certificate ? certificate.name : I18n.t('Apply_Your_Certificate')}</Text> </TouchableOpacity> </View> ); } render() { const { connecting } = this.props; const { text, autoFocus } = this.state; return ( <KeyboardView contentContainerStyle={sharedStyles.container} keyboardVerticalOffset={128} key='login-view' > <StatusBar light /> <SafeAreaView style={sharedStyles.container} testID='new-server-view'> <ScrollView {...scrollPersistTaps} contentContainerStyle={sharedStyles.containerScrollView}> <Image style={styles.image} source={{ uri: 'new_server' }} /> <Text style={styles.title}>{I18n.t('Sign_in_your_server')}</Text> <TextInput autoFocus={autoFocus} containerStyle={styles.inputContainer} placeholder={defaultServer} value={text} returnKeyType='send' onChangeText={this.onChangeText} testID='new-server-view-input' onSubmitEditing={this.submit} clearButtonMode='while-editing' /> <Button title={I18n.t('Connect')} type='primary' onPress={this.submit} disabled={!text} loading={connecting} testID='new-server-view-button' /> {isIOS ? this.renderCertificatePicker() : null} </ScrollView> </SafeAreaView> {this.renderBack()} </KeyboardView> ); } } const mapStateToProps = state => ({ connecting: state.server.connecting }); const mapDispatchToProps = dispatch => ({ connectServer: (server, certificate) => dispatch(serverRequest(server, certificate)) }); export default connect(mapStateToProps, mapDispatchToProps)(NewServerView);