[RN] Validate the URL in app-settings
This commit is contained in:
parent
c4468cb7b8
commit
6a9e6db3be
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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 */
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue