// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; import { ActivityIndicator, DeviceEventEmitter, Image, Keyboard, KeyboardAvoidingView, Platform, StatusBar, StyleSheet, Text, TextInput, TouchableWithoutFeedback, View, } from 'react-native'; import Button from 'react-native-button'; import RNFetchBlob from 'rn-fetch-blob'; import {Client4} from 'mattermost-redux/client'; import ErrorText from 'app/components/error_text'; import FormattedText from 'app/components/formatted_text'; import fetchConfig from 'app/fetch_preconfig'; import mattermostBucket from 'app/mattermost_bucket'; import {GlobalStyles} from 'app/styles'; import {checkUpgradeType, isUpgradeAvailable} from 'app/utils/client_upgrade'; import {isValidUrl, stripTrailingSlashes} from 'app/utils/url'; import {preventDoubleTap} from 'app/utils/tap'; import tracker from 'app/utils/time_tracker'; import {t} from 'app/utils/i18n'; import LocalConfig from 'assets/config'; export default class SelectServer extends PureComponent { static propTypes = { actions: PropTypes.shape({ getPing: PropTypes.func.isRequired, handleServerUrlChanged: PropTypes.func.isRequired, handleSuccessfulLogin: PropTypes.func.isRequired, scheduleExpiredNotification: PropTypes.func.isRequired, loadConfigAndLicense: PropTypes.func.isRequired, login: PropTypes.func.isRequired, resetPing: PropTypes.func.isRequired, setLastUpgradeCheck: PropTypes.func.isRequired, setServerVersion: PropTypes.func.isRequired, }).isRequired, allowOtherServers: PropTypes.bool, config: PropTypes.object, currentVersion: PropTypes.string, hasConfigAndLicense: PropTypes.bool.isRequired, latestVersion: PropTypes.string, license: PropTypes.object, minVersion: PropTypes.string, navigator: PropTypes.object, serverUrl: PropTypes.string.isRequired, theme: PropTypes.object, }; static contextTypes = { intl: intlShape.isRequired, }; constructor(props) { super(props); this.state = { connected: false, connecting: false, error: null, url: props.serverUrl, }; this.cancelPing = null; } componentDidMount() { const {allowOtherServers, serverUrl} = this.props; if (!allowOtherServers && serverUrl) { // If the app is managed or AutoSelectServerUrl is true in the Config, the server url is set and the user can't change it // we automatically trigger the ping to move to the next screen this.handleConnect(); } if (Platform.OS === 'android') { Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); } this.certificateListener = DeviceEventEmitter.addListener('RNFetchBlobCertificate', this.selectCertificate); this.props.navigator.setOnNavigatorEvent(this.handleNavigatorEvent); } componentWillUpdate(nextProps, nextState) { if (nextState.connected && nextProps.hasConfigAndLicense && !(this.state.connected && this.props.hasConfigAndLicense)) { if (LocalConfig.EnableMobileClientUpgrade) { this.props.actions.setLastUpgradeCheck(); const {currentVersion, minVersion, latestVersion} = nextProps; const upgradeType = checkUpgradeType(currentVersion, minVersion, latestVersion); if (isUpgradeAvailable(upgradeType)) { this.handleShowClientUpgrade(upgradeType); } else { this.handleLoginOptions(nextProps); } } else { this.handleLoginOptions(nextProps); } } } componentWillUnmount() { this.certificateListener.remove(); if (Platform.OS === 'android') { Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); } } blur = () => { if (this.textInput) { this.textInput.blur(); } }; getUrl = () => { const urlParse = require('url-parse'); let preUrl = urlParse(this.state.url, true); if (!preUrl.host || preUrl.protocol === 'file:') { preUrl = urlParse('https://' + stripTrailingSlashes(this.state.url), true); } if (preUrl.protocol === 'http:') { preUrl.protocol = 'https:'; } return stripTrailingSlashes(preUrl.protocol + '//' + preUrl.host + preUrl.pathname); }; goToNextScreen = (screen, title) => { const {navigator, theme} = this.props; navigator.push({ screen, title, animated: true, backButtonTitle: '', navigatorStyle: { navBarHidden: LocalConfig.AutoSelectServerUrl, disabledBackGesture: LocalConfig.AutoSelectServerUrl, navBarTextColor: theme.sidebarHeaderTextColor, navBarBackgroundColor: theme.sidebarHeaderBg, navBarButtonColor: theme.sidebarHeaderTextColor, }, }); }; handleAndroidKeyboard = () => { this.blur(); }; handleConnect = preventDoubleTap(async () => { const url = this.getUrl(); Keyboard.dismiss(); if (this.state.connecting || this.state.connected) { this.cancelPing(); return; } if (!isValidUrl(url)) { this.setState({ error: { intl: { id: t('mobile.server_url.invalid_format'), defaultMessage: 'URL must start with http:// or https://', }, }, }); return; } if (LocalConfig.ExperimentalClientSideCertEnable && Platform.OS === 'ios') { RNFetchBlob.cba.selectCertificate((certificate) => { if (certificate) { mattermostBucket.setPreference('cert', certificate); window.fetch = new RNFetchBlob.polyfill.Fetch({ auto: true, certificate, }).build(); this.pingServer(url); } }); } else { this.pingServer(url); } }); handleLoginOptions = (props = this.props) => { const {formatMessage} = this.context.intl; const {config, license} = props; const samlEnabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true'; const gitlabEnabled = config.EnableSignUpWithGitLab === 'true'; let options = 0; if (samlEnabled || gitlabEnabled) { options += 1; } let screen; let title; if (options) { screen = 'LoginOptions'; title = formatMessage({id: 'mobile.routes.loginOptions', defaultMessage: 'Login Chooser'}); } else { screen = 'Login'; title = formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'}); } this.props.actions.resetPing(); if (Platform.OS === 'ios') { if (config.ExperimentalClientSideCertEnable === 'true' && config.ExperimentalClientSideCertCheck === 'primary') { // log in automatically and send directly to the channel screen this.loginWithCertificate(); return; } setTimeout(() => { this.goToNextScreen(screen, title); }, 350); } else { this.goToNextScreen(screen, title); } }; handleNavigatorEvent = (event) => { switch (event.id) { case 'didDisappear': this.setState({ connected: false, }); break; } }; handleShowClientUpgrade = (upgradeType) => { const {formatMessage} = this.context.intl; const {theme} = this.props; this.props.navigator.push({ screen: 'ClientUpgrade', title: formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Client Upgrade'}), backButtonTitle: '', navigatorStyle: { navBarHidden: LocalConfig.AutoSelectServerUrl, disabledBackGesture: LocalConfig.AutoSelectServerUrl, statusBarHidden: true, statusBarHideWithNavBar: true, navBarTextColor: theme.sidebarHeaderTextColor, navBarBackgroundColor: theme.sidebarHeaderBg, navBarButtonColor: theme.sidebarHeaderTextColor, }, passProps: { closeAction: this.handleLoginOptions, upgradeType, }, }); }; handleTextChanged = (url) => { this.setState({url}); }; inputRef = (ref) => { this.textInput = ref; }; loginWithCertificate = async () => { const {navigator} = this.props; tracker.initialLoad = Date.now(); await this.props.actions.login('credential', 'password'); await this.props.actions.handleSuccessfulLogin(); this.scheduleSessionExpiredNotification(); navigator.resetTo({ screen: 'Channel', title: '', animated: false, backButtonTitle: '', navigatorStyle: { animated: true, animationType: 'fade', navBarHidden: true, statusBarHidden: false, statusBarHideWithNavBar: false, screenBackgroundColor: 'transparent', }, }); }; pingServer = (url, retryWithHttp = true) => { const { getPing, handleServerUrlChanged, loadConfigAndLicense, setServerVersion, } = this.props.actions; this.setState({ connected: false, connecting: true, error: null, }); Client4.setUrl(url); Client4.online = true; handleServerUrlChanged(url); let cancel = false; this.cancelPing = () => { cancel = true; this.setState({ connected: false, connecting: false, }); this.cancelPing = null; }; getPing().then((result) => { if (cancel) { return; } if (result.error && retryWithHttp) { this.pingServer(url.replace('https:', 'http:'), false); return; } if (!result.error) { loadConfigAndLicense(); setServerVersion(Client4.getServerVersion()); } this.setState({ connected: !result.error, connecting: false, error: result.error, }); }).catch(() => { if (cancel) { return; } this.setState({ connecting: false, }); }); }; scheduleSessionExpiredNotification = () => { const {intl} = this.context; const {actions} = this.props; actions.scheduleExpiredNotification(intl); }; selectCertificate = () => { const url = this.getUrl(); RNFetchBlob.cba.selectCertificate((certificate) => { if (certificate) { mattermostBucket.setPreference('cert', certificate); fetchConfig().then(() => { this.pingServer(url, true); }); } }); }; render() { const {formatMessage} = this.context.intl; const {allowOtherServers} = this.props; const { connected, connecting, error, url, } = this.state; let buttonIcon; let buttonText; if (connected || connecting) { buttonIcon = ( ); buttonText = ( ); } else { buttonText = ( ); } let statusStyle = 'dark-content'; if (Platform.OS === 'android') { statusStyle = 'light-content'; } const inputDisabled = !allowOtherServers || connected || connecting; const inputStyle = [GlobalStyles.inputBox]; if (inputDisabled) { inputStyle.push(style.disabledInput); } return ( ); } } const style = StyleSheet.create({ container: { flex: 1, }, disabledInput: { backgroundColor: '#e3e3e3', }, connectButton: { alignItems: 'center', }, connectingIndicator: { marginRight: 5, }, });