[RN] Validate the URL in app-settings

This commit is contained in:
zbettenbuk 2018-01-26 11:19:43 +01:00 committed by Saúl Ibarra Corretgé
parent c4468cb7b8
commit 6a9e6db3be
9 changed files with 180 additions and 44 deletions

View File

@ -539,6 +539,9 @@
"tooltip": "Get access info about the meeting" "tooltip": "Get access info about the meeting"
}, },
"profileModal": { "profileModal": {
"alertOk": "OK",
"alertTitle": "Warning",
"alertURLText": "The entered server URL is invalid",
"conferenceSection": "Conference", "conferenceSection": "Conference",
"displayName": "Display name", "displayName": "Display name",
"email": "Email", "email": "Email",

View File

@ -4,8 +4,6 @@ import { Component } from 'react';
import { getProfile, updateProfile } from '../../base/profile'; import { getProfile, updateProfile } from '../../base/profile';
import { hideAppSettings } from '../actions';
/** /**
* The type of the React {@code Component} props of {@link AbstractAppSettings} * The type of the React {@code Component} props of {@link AbstractAppSettings}
*/ */
@ -34,7 +32,12 @@ type Props = {
/** /**
* Redux store dispatch function. * Redux store dispatch function.
*/ */
dispatch: Dispatch<*> dispatch: Dispatch<*>,
/**
* The i18n translate function.
*/
t: Function
}; };
/** /**
@ -44,6 +47,7 @@ type Props = {
* @abstract * @abstract
*/ */
export class AbstractAppSettings extends Component<Props> { export class AbstractAppSettings extends Component<Props> {
/** /**
* Initializes a new {@code AbstractAppSettings} instance. * Initializes a new {@code AbstractAppSettings} instance.
* *
@ -56,7 +60,6 @@ export class AbstractAppSettings extends Component<Props> {
this._onChangeDisplayName = this._onChangeDisplayName.bind(this); this._onChangeDisplayName = this._onChangeDisplayName.bind(this);
this._onChangeEmail = this._onChangeEmail.bind(this); this._onChangeEmail = this._onChangeEmail.bind(this);
this._onChangeServerURL = this._onChangeServerURL.bind(this); this._onChangeServerURL = this._onChangeServerURL.bind(this);
this._onRequestClose = this._onRequestClose.bind(this);
this._onStartAudioMutedChange this._onStartAudioMutedChange
= this._onStartAudioMutedChange.bind(this); = this._onStartAudioMutedChange.bind(this);
this._onStartVideoMutedChange this._onStartVideoMutedChange
@ -108,17 +111,6 @@ export class AbstractAppSettings extends Component<Props> {
}); });
} }
_onRequestClose: () => void;
/**
* Handles the back button.
*
* @returns {void}
*/
_onRequestClose() {
this.props.dispatch(hideAppSettings());
}
_onStartAudioMutedChange: (boolean) => void; _onStartAudioMutedChange: (boolean) => void;
/** /**

View File

@ -1,5 +1,8 @@
// @flow
import React from 'react'; import React from 'react';
import { import {
Alert,
Modal, Modal,
ScrollView, ScrollView,
Switch, Switch,
@ -11,13 +14,14 @@ import { connect } from 'react-redux';
import { ASPECT_RATIO_NARROW } from '../../base/aspect-ratio'; import { ASPECT_RATIO_NARROW } from '../../base/aspect-ratio';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { isIPad } from '../../base/react'; import { getSafetyOffset, isIPad } from '../../base/react';
import { _mapStateToProps, AbstractAppSettings } from './AbstractAppSettings'; import { _mapStateToProps, AbstractAppSettings } from './AbstractAppSettings';
import BackButton from './BackButton'; import { hideAppSettings } from '../actions';
import FormRow from './FormRow'; import BackButton from './BackButton.native';
import FormSectionHeader from './FormSectionHeader'; import FormRow from './FormRow.native';
import { getSafetyOffset } from '../functions'; import FormSectionHeader from './FormSectionHeader.native';
import { normalizeUserInputURL } from '../functions';
import styles, { HEADER_PADDING } from './styles'; import styles, { HEADER_PADDING } from './styles';
/** /**
@ -26,6 +30,8 @@ import styles, { HEADER_PADDING } from './styles';
* @extends AbstractAppSettings * @extends AbstractAppSettings
*/ */
class AppSettings extends AbstractAppSettings { class AppSettings extends AbstractAppSettings {
_urlField: Object;
/** /**
* Instantiates a new {@code AppSettings} instance. * Instantiates a new {@code AppSettings} instance.
* *
@ -35,6 +41,10 @@ class AppSettings extends AbstractAppSettings {
super(props); super(props);
this._getSafetyPadding = this._getSafetyPadding.bind(this); this._getSafetyPadding = this._getSafetyPadding.bind(this);
this._onBlurServerURL = this._onBlurServerURL.bind(this);
this._onRequestClose = this._onRequestClose.bind(this);
this._setURLFieldReference = this._setURLFieldReference.bind(this);
this._showURLAlert = this._showURLAlert.bind(this);
} }
/** /**
@ -97,8 +107,10 @@ class AppSettings extends AbstractAppSettings {
i18nLabel = 'profileModal.serverURL' > i18nLabel = 'profileModal.serverURL' >
<TextInput <TextInput
autoCapitalize = 'none' autoCapitalize = 'none'
onBlur = { this._onBlurServerURL }
onChangeText = { this._onChangeServerURL } onChangeText = { this._onChangeServerURL }
placeholder = { this.props._serverURL } placeholder = { this.props._serverURL }
ref = { this._setURLFieldReference }
value = { _profile.serverURL } /> value = { _profile.serverURL } />
</FormRow> </FormRow>
<FormRow <FormRow
@ -127,6 +139,8 @@ class AppSettings extends AbstractAppSettings {
); );
} }
_getSafetyPadding: () => Object;
/** /**
* Calculates header safety padding for mobile devices. See comment in * Calculates header safety padding for mobile devices. See comment in
* functions.js. * functions.js.
@ -145,6 +159,100 @@ class AppSettings extends AbstractAppSettings {
return undefined; return undefined;
} }
_onBlurServerURL: () => void;
/**
* Handler the server URL lose focus event. Here we validate the server URL
* and update it to the normalized version, or show an error if incorrect.
*
* @private
* @returns {void}
*/
_onBlurServerURL() {
this._processServerURL(false /* hideOnSuccess */);
}
_onChangeDisplayName: (string) => void;
_onChangeEmail: (string) => void;
_onChangeServerURL: (string) => void;
_onStartAudioMutedChange: (boolean) => void;
_onStartVideoMutedChange: (boolean) => void;
_onRequestClose: () => void;
/**
* Processes the server URL. It normalizes it and an error alert is
* displayed in case it's incorrect.
*
* @param {boolean} hideOnSuccess - True if the dialog should be hidden if
* normalization / validation succeeds, false otherwise.
* @private
* @returns {void}
*/
_processServerURL(hideOnSuccess: boolean) {
const { serverURL } = this.props._profile;
const normalizedURL = normalizeUserInputURL(serverURL);
if (normalizedURL === null) {
this._showURLAlert();
} else {
this._onChangeServerURL(normalizedURL);
if (hideOnSuccess) {
this.props.dispatch(hideAppSettings());
}
}
}
/**
* Handles the back button.
* Also invokes normalizeUserInputURL to validate the URL entered
* by the user.
*
* @returns {void}
*/
_onRequestClose() {
this._processServerURL(true /* hideOnSuccess */);
}
_setURLFieldReference: (React$ElementRef<*> | null) => void;
/**
* Stores a reference to the URL field for later use.
*
* @protected
* @param {Object} component - The field component.
* @returns {void}
*/
_setURLFieldReference(component) {
this._urlField = component;
}
_showURLAlert: () => void;
/**
* Shows an alert telling the user that the URL he/she entered was invalid.
*
* @returns {void}
*/
_showURLAlert() {
const { t } = this.props;
Alert.alert(
t('profileModal.alertTitle'),
t('profileModal.alertURLText'),
[
{
onPress: () => this._urlField.focus(),
text: t('profileModal.alertOk')
}
]
);
}
} }
export default translate(connect(_mapStateToProps)(AppSettings)); export default translate(connect(_mapStateToProps)(AppSettings));

View File

@ -6,8 +6,8 @@ import { connect } from 'react-redux';
import { ASPECT_RATIO_WIDE } from '../../base/aspect-ratio'; import { ASPECT_RATIO_WIDE } from '../../base/aspect-ratio';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { getSafetyOffset } from '../../base/react';
import { getSafetyOffset } from '../functions';
import styles, { ANDROID_UNDERLINE_COLOR, CONTAINER_PADDING } from './styles'; import styles, { ANDROID_UNDERLINE_COLOR, CONTAINER_PADDING } from './styles';
/** /**

View File

@ -6,8 +6,8 @@ import { connect } from 'react-redux';
import { ASPECT_RATIO_WIDE } from '../../base/aspect-ratio'; import { ASPECT_RATIO_WIDE } from '../../base/aspect-ratio';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { getSafetyOffset } from '../../base/react';
import { getSafetyOffset } from '../functions';
import styles, { CONTAINER_PADDING } from './styles'; import styles, { CONTAINER_PADDING } from './styles';
/** /**

View File

@ -0,0 +1,37 @@
// @flow
import { parseStandardURIString } from '../base/util';
/**
* Normalizes a URL entered by the user.
* FIXME: Consider adding this to base/util/uri.
*
* @param {string} url - The URL to validate.
* @returns {string|null} - The normalized URL, or null if the URL is invalid.
*/
export function normalizeUserInputURL(url: string) {
/* eslint-disable no-param-reassign */
if (url) {
url = url.replace(/\s/g, '').toLowerCase();
const urlRegExp = new RegExp('^(\\w+://)?(.+)$');
const urlComponents = urlRegExp.exec(url);
if (!urlComponents[1] || !urlComponents[1].startsWith('http')) {
url = `https://${urlComponents[2]}`;
}
const parsedURI
= parseStandardURIString(url);
if (!parsedURI.host) {
return null;
}
return parsedURI.toString();
}
return url;
/* eslint-enable no-param-reassign */
}

View File

@ -1,22 +0,0 @@
// @flow
import { isIPhoneX, Platform } from '../base/react';
const IPHONE_OFFSET = 20;
const IPHONEX_OFFSET = 44;
/**
* Determines the offset to be used for the device. This uses a custom
* implementation to minimize empty area around screen, especially on iPhone X.
*
* @returns {number}
*/
export function getSafetyOffset() {
if (Platform.OS === 'android') {
// Android doesn't need offset, except the Essential phone. Should be
// addressed later with a generic solution.
return 0;
}
return isIPhoneX() ? IPHONEX_OFFSET : IPHONE_OFFSET;
}

View File

@ -6,6 +6,24 @@ import Platform from './Platform';
const IPHONEX_HEIGHT = 812; const IPHONEX_HEIGHT = 812;
const IPHONEX_WIDTH = 375; const IPHONEX_WIDTH = 375;
const IPHONE_OFFSET = 20;
const IPHONEX_OFFSET = 44;
/**
* Determines the offset to be used for the device. This uses a custom
* implementation to minimize empty area around screen, especially on iPhone X.
*
* @returns {number}
*/
export function getSafetyOffset() {
if (Platform.OS === 'android') {
// Android doesn't need offset, except the Essential phone. Should be
// addressed later with a generic solution.
return 0;
}
return isIPhoneX() ? IPHONEX_OFFSET : IPHONE_OFFSET;
}
/** /**
* Determines if the device is an iPad or not. * Determines if the device is an iPad or not.