diff --git a/lang/main.json b/lang/main.json
index bdb6b4344..de572067e 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -264,7 +264,7 @@
"removeSharedVideoMsg": "Are you sure you would like to remove your shared video?",
"alreadySharedVideoMsg": "Another member is already sharing video. This conference allows only one shared video at a time.",
"WaitingForHost": "Waiting for the host ...",
- "WaitForHostMsg": "The conference __room__ has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
+ "WaitForHostMsg": "The conference '__room__' has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
"IamHost": "I am the host",
"Cancel": "Cancel",
"Submit": "Submit",
diff --git a/modules/UI/authentication/AuthHandler.js b/modules/UI/authentication/AuthHandler.js
index 74981f717..fd9c1b664 100644
--- a/modules/UI/authentication/AuthHandler.js
+++ b/modules/UI/authentication/AuthHandler.js
@@ -140,44 +140,35 @@ function initJWTTokenListener(room) {
* @param {string} [lockPassword] password to use if the conference is locked
*/
function doXmppAuth (room, lockPassword) {
- let loginDialog = LoginDialog.showAuthDialog(function (id, password) {
- // auth "on the fly":
- // 1. open new connection with proper id and password
- // 2. connect to the room
- // (this will store sessionId in the localStorage)
- // 3. close new connection
- // 4. reallocate focus in current room
- openConnection({id, password, roomName: room.getName()}).then(
- function (connection) {
- // open room
- let newRoom = connection.initJitsiConference(
- room.getName(), APP.conference._getConferenceOptions()
- );
-
- loginDialog.displayConnectionStatus('connection.FETCH_SESSION_ID');
-
- newRoom.room.moderator.authenticate().then(function () {
- connection.disconnect();
+ const loginDialog = LoginDialog.showAuthDialog(function (id, password) {
+ const authConnection = room.createAuthenticationConnection();
+ authConnection.authenticateAndUpgradeRole({
+ id,
+ password,
+ roomPassword: lockPassword,
+ onLoginSuccessful: () => { /* Called when XMPP login succeeds */
+ loginDialog.displayConnectionStatus(
+ 'connection.FETCH_SESSION_ID');
+ }
+ })
+ .then(() => {
loginDialog.displayConnectionStatus(
'connection.GOT_SESSION_ID');
-
- // authenticate conference on the fly
- room.join(lockPassword);
-
loginDialog.close();
- }).catch(function (error, code) {
- connection.disconnect();
-
- logger.error('Auth on the fly failed', error);
-
- loginDialog.displayError(
- 'connection.GET_SESSION_ID_ERROR', {code: code});
+ })
+ .catch(error => {
+ logger.error('authenticateAndUpgradeRole failed', error);
+ if (error.authenticationError) {
+ loginDialog.displayError(
+ 'connection.GET_SESSION_ID_ERROR', {
+ msg: error.authenticationError
+ });
+ } else {
+ loginDialog.displayError(error.connectionError);
+ }
});
- }, function (err) {
- loginDialog.displayError(err);
- });
- }, function () { // user canceled
+ }, function () {
loginDialog.close();
});
}
diff --git a/modules/UI/authentication/LoginDialog.js b/modules/UI/authentication/LoginDialog.js
index 08bf9f71d..1e1426214 100644
--- a/modules/UI/authentication/LoginDialog.js
+++ b/modules/UI/authentication/LoginDialog.js
@@ -1,4 +1,6 @@
/* global $, APP, config, JitsiMeetJS */
+import { toJid } from '../../../react/features/base/connection';
+
const ConnectionErrors = JitsiMeetJS.errors.connection;
/**
@@ -19,26 +21,6 @@ function getPasswordInputHtml() {
data-i18n="[placeholder]dialog.userPassword">`;
}
-/**
- * Convert provided id to jid if it's not jid yet.
- * @param {string} id user id or jid
- * @returns {string} jid
- */
-function toJid(id) {
- if (id.indexOf("@") >= 0) {
- return id;
- }
-
- let jid = id.concat('@');
- if (config.hosts.authdomain) {
- jid += config.hosts.authdomain;
- } else {
- jid += config.hosts.domain;
- }
-
- return jid;
-}
-
/**
* Generate cancel button config for the dialog.
* @returns {Object}
@@ -90,7 +72,7 @@ function LoginDialog(successCallback, cancelCallback) {
let password = f.password;
if (jid && password) {
connDialog.goToState('connecting');
- successCallback(toJid(jid), password);
+ successCallback(toJid(jid, config.hosts), password);
}
} else {
// User cancelled
@@ -223,7 +205,7 @@ export default {
*/
showAuthRequiredDialog: function (roomName, onAuthNow) {
var msg = APP.translation.generateTranslationHTML(
- "[html]dialog.WaitForHostMsg", {room: roomName}
+ "dialog.WaitForHostMsg", {room: roomName}
);
var buttonTxt = APP.translation.generateTranslationHTML(
diff --git a/react/features/app/components/App.native.js b/react/features/app/components/App.native.js
index fdbc2d32e..6a5ac264f 100644
--- a/react/features/app/components/App.native.js
+++ b/react/features/app/components/App.native.js
@@ -15,6 +15,7 @@ import '../../mobile/proximity';
import '../../mobile/wake-lock';
import { AbstractApp } from './AbstractApp';
+import '../../authentication';
/**
* Root application component.
diff --git a/react/features/authentication/actionTypes.js b/react/features/authentication/actionTypes.js
new file mode 100644
index 000000000..db93e681e
--- /dev/null
+++ b/react/features/authentication/actionTypes.js
@@ -0,0 +1,75 @@
+/**
+ * The type of (redux) action which signals that {@link LoginDialog} has been
+ * canceled.
+ *
+ * {
+ * type: CANCEL_LOGIN
+ * }
+ */
+export const CANCEL_LOGIN = Symbol('CANCEL_LOGIN');
+
+/**
+ * The type of (redux) action which signals that the {@link WaitForOwnerDialog}
+ * has been canceled.
+ *
+ * {
+ * type: CANCEL_WAIT_FOR_OWNER
+ * }
+ */
+export const CANCEL_WAIT_FOR_OWNER = Symbol('CANCEL_WAIT_FOR_OWNER');
+
+/**
+ * The type of (redux) action which signals that the cyclic operation of waiting
+ * for conference owner has been aborted.
+ *
+ * {
+ * type: STOP_WAIT_FOR_OWNER
+ * }
+ */
+export const STOP_WAIT_FOR_OWNER = Symbol('STOP_WAIT_FOR_OWNER');
+
+/**
+ * The type of (redux) action which signals that the process of authenticating
+ * and upgrading the current conference user's role has been started.
+ *
+ * {
+ * type: UPGRADE_ROLE_STARTED,
+ * authConnection: JitsiAuthConnection
+ * }
+ */
+export const UPGRADE_ROLE_STARTED = Symbol('UPGRADE_ROLE_STARTED');
+
+/**
+ * The type of (redux) action which informs that the authentication and role
+ * upgrade process has been completed successfully.
+ *
+ * {
+ * type: UPGRADE_ROLE_SUCCESS
+ * }
+ */
+export const UPGRADE_ROLE_SUCCESS = Symbol('UPGRADE_ROLE_SUCCESS');
+
+/**
+ * The type of (redux) action which informs that the authentication and role
+ * upgrade process has failed with an error. Check the docs of
+ * {@link JitsiAuthConnection} for more details about the error structure.
+ *
+ * {
+ * type: UPGRADE_ROLE_SUCCESS,
+ * error: Object
+ * }
+ */
+export const UPGRADE_ROLE_FAILED = Symbol('UPGRADE_ROLE_FAILED');
+
+/**
+ * The type of (redux) action that sets delayed handler which will check if
+ * the conference has been created and it's now possible to join from anonymous
+ * connection.
+ *
+ * {
+ * type: WAIT_FOR_OWNER,
+ * handler: Function,
+ * timeoutMs: number
+ * }
+ */
+export const WAIT_FOR_OWNER = Symbol('WAIT_FOR_OWNER');
diff --git a/react/features/authentication/actions.js b/react/features/authentication/actions.js
new file mode 100644
index 000000000..1f6f8bac1
--- /dev/null
+++ b/react/features/authentication/actions.js
@@ -0,0 +1,210 @@
+import { openDialog } from '../base/dialog/actions';
+import { checkIfCanJoin } from '../base/conference/actions';
+import {
+ CANCEL_LOGIN,
+ CANCEL_WAIT_FOR_OWNER,
+ STOP_WAIT_FOR_OWNER,
+ UPGRADE_ROLE_FAILED,
+ UPGRADE_ROLE_STARTED,
+ UPGRADE_ROLE_SUCCESS,
+ WAIT_FOR_OWNER
+} from './actionTypes';
+import { LoginDialog, WaitForOwnerDialog } from './components';
+
+/**
+ * Instantiates new {@link JitsiAuthConnection} and uses it to authenticate and
+ * upgrade role of the current conference user to moderator which will allow to
+ * create and join new conference on XMPP password + guest access configuration.
+ * See {@link LoginDialog} description for more info.
+ *
+ * @param {string} id - XMPP user's id eg. user@domain.com.
+ * @param {string} userPassword - The user's password.
+ * @param {JitsiConference} conference - The conference for which user's role
+ * will be upgraded.
+ * @returns {function({dispatch: Function, getState: Function})}
+ */
+export function authenticateAndUpgradeRole(id, userPassword, conference) {
+ return (dispatch, getState) => {
+ const authConnection = conference.createAuthenticationConnection();
+
+ dispatch(_upgradeRoleStarted(authConnection));
+
+ const { password: roomPassword }
+ = getState()['features/base/conference'];
+
+ authConnection.authenticateAndUpgradeRole({
+ id,
+ password: userPassword,
+ roomPassword
+ })
+ .then(() => {
+ dispatch(_upgradeRoleSuccess());
+ })
+ .catch(error => {
+ // Lack of error means the operation was canceled, so no need to log
+ // that on error level.
+ if (error.error) {
+ console.error('upgradeRoleFailed', error);
+ }
+ dispatch(_upgradeRoleFailed(error));
+ });
+ };
+}
+
+/**
+ * Cancels {@ink LoginDialog}.
+ *
+ * @returns {{
+ * type: CANCEL_LOGIN
+ * }}
+ */
+export function cancelLogin() {
+ return {
+ type: CANCEL_LOGIN
+ };
+}
+
+/**
+ * Cancels {@link WaitForOwnerDialog}. Will navigate back to the welcome page.
+ *
+ * @returns {{
+ * type: CANCEL_WAIT_FOR_OWNER
+ * }}
+ */
+export function cancelWaitForOwner() {
+ return {
+ type: CANCEL_WAIT_FOR_OWNER
+ };
+}
+
+/**
+ * Stops waiting for conference owner and clears any pending timeout.
+ *
+ * @returns {{
+ * type: STOP_WAIT_FOR_OWNER
+ * }}
+ */
+export function clearWaitForOwnerTimeout() {
+ return {
+ type: STOP_WAIT_FOR_OWNER
+ };
+}
+
+/**
+ * Sets a delayed "wait for owner" handler function.
+ *
+ * @param {Function} handler - The "wait for owner" handler function.
+ * @param {number} waitMs - The delay in milliseconds.
+ *
+ * @private
+ * @returns {{
+ * type: WAIT_FOR_OWNER,
+ * handler: Function,
+ * timeoutMs: number
+ * }}
+ */
+function _setWaitForOwnerTimeout(handler, waitMs) {
+ return {
+ type: WAIT_FOR_OWNER,
+ handler,
+ timeoutMs: waitMs
+ };
+}
+
+/**
+ * Displays {@link LoginDialog} which will ask to enter username and password
+ * for the current conference.
+ *
+ * @protected
+ * @returns {{
+ * type: OPEN_DIALOG,
+ * component: LoginDialog,
+ * props: React.PropTypes
+ * }}
+ */
+export function _showLoginDialog() {
+ return openDialog(LoginDialog, { });
+}
+
+/**
+ * Displays {@link WaitForOnwerDialog}.
+ *
+ * @protected
+ * @returns {{
+ * type: OPEN_DIALOG,
+ * component: WaitForOwnerDialog,
+ * props: React.PropTypes
+ * }}
+ */
+export function _showWaitForOwnerDialog() {
+ return openDialog(WaitForOwnerDialog, { });
+}
+
+/**
+ * Emits an error which occurred during {@link authenticateAndUpgradeRole}.
+ *
+ * @param {Object} error - Check the docs of {@link JitsiAuthConnection} in
+ * lib-jitsi-meet for more details about the error's structure.
+ *
+ * @private
+ * @returns {{
+ * type: UPGRADE_ROLE_FAILED,
+ * error: Object
+ * }}
+ */
+function _upgradeRoleFailed(error) {
+ return {
+ type: UPGRADE_ROLE_FAILED,
+ error
+ };
+}
+
+/**
+ * Signals that the role upgrade process has been started using given
+ * {@link JitsiAuthConnection} instance.
+ *
+ * @param {JitsiAuthConnection} authenticationConnection - The authentication
+ * connection instance that can be used to cancel the process.
+ *
+ * @private
+ * @returns {{
+ * type: UPGRADE_ROLE_STARTED,
+ * authConnection: JitsiAuthConnection
+ * }}
+ */
+function _upgradeRoleStarted(authenticationConnection) {
+ return {
+ type: UPGRADE_ROLE_STARTED,
+ authConnection: authenticationConnection
+ };
+}
+
+/**
+ * Signals that the role upgrade process has been completed successfully.
+ *
+ * @private
+ * @returns {{
+ * type: UPGRADE_ROLE_SUCCESS
+ * }}
+ */
+function _upgradeRoleSuccess() {
+ return {
+ type: UPGRADE_ROLE_SUCCESS
+ };
+}
+
+/**
+ * Called when Jicofo rejects to create the room for anonymous user. Will
+ * start the process of "waiting for the owner" by periodically trying to join
+ * the room every five seconds.
+ *
+ * @returns {function({ dispatch: Function})}
+ */
+export function waitForOwner() {
+ return dispatch => {
+ dispatch(
+ _setWaitForOwnerTimeout(
+ () => dispatch(checkIfCanJoin()),
+ 5000));
+ };
+}
diff --git a/react/features/authentication/components/LoginDialog.native.js b/react/features/authentication/components/LoginDialog.native.js
new file mode 100644
index 000000000..9a4f0652d
--- /dev/null
+++ b/react/features/authentication/components/LoginDialog.native.js
@@ -0,0 +1,280 @@
+import React, { Component } from 'react';
+import { connect as reduxConnect } from 'react-redux';
+import {
+ Button,
+ Modal,
+ Text,
+ TextInput,
+ View
+} from 'react-native';
+import {
+ authenticateAndUpgradeRole,
+ cancelLogin
+} from '../actions';
+import {
+ connect,
+ toJid
+} from '../../base/connection';
+import { translate } from '../../base/i18n';
+import { JitsiConnectionErrors } from '../../base/lib-jitsi-meet';
+import styles from './styles';
+
+/**
+ * Dialog asks user for username and password.
+ *
+ * First authentication configuration that it will deal with is the main XMPP
+ * domain (config.hosts.domain) with password authentication. A LoginDialog
+ * will be opened after 'CONNECTION_FAILED' action with
+ * 'JitsiConnectionErrors.PASSWORD_REQUIRED' error. After username and password
+ * are entered a new 'connect' action from 'features/base/connection' will be
+ * triggered which will result in new XMPP connection. The conference will start
+ * if the credentials are correct.
+ *
+ * The second setup is the main XMPP domain with password plus guest domain with
+ * anonymous access configured under 'config.hosts.anonymousdomain'. In such
+ * case user connects from the anonymous domain, but if the room does not exist
+ * yet, Jicofo will not allow to start new conference. This will trigger
+ * 'CONFERENCE_FAILED' action with JitsiConferenceErrors.AUTHENTICATION_REQUIRED
+ * error and 'authRequired' value of 'features/base/conference' will hold
+ * the {@link JitsiConference} instance. If user decides to authenticate a new
+ * {@link JitsiAuthConnection} will be created from which separate XMPP
+ * connection is established and authentication is performed. In case it
+ * succeeds Jicofo will assign new session ID which then can be used from
+ * the anonymous domain connection to create and join the room. This part is
+ * done by {@link JitsiAuthConnection} from lib-jitsi-meet.
+ *
+ * See https://github.com/jitsi/jicofo#secure-domain for configuration
+ * parameters description.
+ */
+class LoginDialog extends Component {
+ /**
+ * LoginDialog component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * {@link JitsiConference} that needs authentication - will hold a valid
+ * value in XMPP login + guest access mode.
+ */
+ conference: React.PropTypes.object,
+
+ /**
+ *
+ */
+ configHosts: React.PropTypes.object,
+
+ /**
+ * Indicates if the dialog should display "connecting" status message.
+ */
+ connecting: React.PropTypes.bool,
+
+ /**
+ * Redux store dispatch method.
+ */
+ dispatch: React.PropTypes.func,
+
+ /**
+ * The error which occurred during login/authentication.
+ */
+ error: React.PropTypes.string,
+
+ /**
+ * Any extra details about the error provided by lib-jitsi-meet.
+ */
+ errorDetails: React.PropTypes.string,
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: React.PropTypes.func
+ };
+
+ /**
+ * Initializes a new LoginDialog instance.
+ *
+ * @param {Object} props - The read-only properties with which the new
+ * instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ // Bind event handlers so they are only bound once for every instance.
+ this._onCancel = this._onCancel.bind(this);
+ this._onLogin = this._onLogin.bind(this);
+ this._onUsernameChange = this._onUsernameChange.bind(this);
+ this._onPasswordChange = this._onPasswordChange.bind(this);
+
+ this.state = {
+ username: '',
+ password: ''
+ };
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const {
+ error,
+ errorDetails,
+ connecting,
+ t
+ } = this.props;
+
+ let messageKey = '';
+ const messageOptions = { };
+
+ if (error === JitsiConnectionErrors.PASSWORD_REQUIRED) {
+ messageKey = 'dialog.incorrectPassword';
+ } else if (error) {
+ messageKey = 'dialog.connectErrorWithMsg';
+
+ messageOptions.msg = `${error} ${errorDetails}`;
+ }
+
+ return (
+
+
+ Username:
+
+ Password:
+
+
+ {error ? t(messageKey, messageOptions) : ''}
+ {connecting && !error
+ ? t('connection.CONNECTING') : ''}
+
+
+
+
+
+ );
+ }
+
+ /**
+ * Called when user edits the username.
+ *
+ * @param {string} text - A new username value entered by user.
+ * @returns {void}
+ * @private
+ */
+ _onUsernameChange(text) {
+ this.setState({
+ username: text
+ });
+ }
+
+ /**
+ * Called when user edits the password.
+ *
+ * @param {string} text - A new password value entered by user.
+ * @returns {void}
+ * @private
+ */
+ _onPasswordChange(text) {
+ this.setState({
+ password: text
+ });
+ }
+
+ /**
+ * Notifies this LoginDialog that it has been dismissed by cancel.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onCancel() {
+ this.props.dispatch(cancelLogin());
+ }
+
+ /**
+ * Notifies this LoginDialog that the login button (OK) has been pressed by
+ * the user.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onLogin() {
+ const conference = this.props.conference;
+ const { username, password } = this.state;
+ const jid = toJid(username, this.props.configHosts);
+
+ // If there's a conference it means that the connection has succeeded,
+ // but authentication is required in order to join the room.
+ if (conference) {
+ this.props.dispatch(
+ authenticateAndUpgradeRole(jid, password, conference));
+ } else {
+ this.props.dispatch(connect(jid, password));
+ }
+ }
+}
+
+/**
+ * Maps (parts of) the Redux state to the associated props for the
+ * {@code LoginDialog} component.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {{
+ * configHosts: Object,
+ * connecting: boolean,
+ * error: string,
+ * errorDetails: string,
+ * conference: JitsiConference
+ * }}
+ */
+function _mapStateToProps(state) {
+ const { hosts: configHosts } = state['features/base/config'];
+ const {
+ connecting,
+ error: connectionError,
+ errorMessage: connectionErrorMessage
+ } = state['features/base/connection'];
+ const {
+ authRequired
+ } = state['features/base/conference'];
+ const {
+ upgradeRoleError,
+ upgradeRoleInProgress
+ } = state['features/authentication'];
+
+ const error
+ = connectionError
+ || (upgradeRoleError
+ && (upgradeRoleError.connectionError
+ || upgradeRoleError.authenticationError));
+
+ return {
+ configHosts,
+ connecting: Boolean(connecting) || Boolean(upgradeRoleInProgress),
+ error,
+ errorDetails:
+ (connectionError && connectionErrorMessage)
+ || (upgradeRoleError && upgradeRoleError.message),
+ conference: authRequired
+ };
+}
+
+export default translate(reduxConnect(_mapStateToProps)(LoginDialog));
diff --git a/react/features/authentication/components/WaitForOwnerDialog.native.js b/react/features/authentication/components/WaitForOwnerDialog.native.js
new file mode 100644
index 000000000..0fc62105a
--- /dev/null
+++ b/react/features/authentication/components/WaitForOwnerDialog.native.js
@@ -0,0 +1,127 @@
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { Button, Modal, Text, View } from 'react-native';
+
+import { translate } from '../../base/i18n';
+import { _showLoginDialog, cancelWaitForOwner } from '../actions';
+import styles from './styles';
+
+/**
+ * The dialog is display in XMPP password + guest access configuration, after
+ * user connects from anonymous domain and the conference does not exist yet.
+ *
+ * See {@link LoginDialog} description for more details.
+ */
+class WaitForOwnerDialog extends Component {
+ /**
+ * WaitForOwnerDialog component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * Redux store dispatch function.
+ */
+ dispatch: React.PropTypes.func,
+
+ /**
+ * The name of the conference room (without the domain part).
+ */
+ roomName: React.PropTypes.string,
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: React.PropTypes.func
+ };
+
+ /**
+ * Initializes a new WaitForWonderDialog instance.
+ *
+ * @param {Object} props - The read-only properties with which the new
+ * instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ this._onLogin = this._onLogin.bind(this);
+ this._onCancel = this._onCancel.bind(this);
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const {
+ roomName,
+ t
+ } = this.props;
+
+ return (
+
+
+
+ { t(
+ 'dialog.WaitForHostMsg',
+ { room: roomName })
+ }
+
+
+
+
+
+ );
+ }
+
+ /**
+ * Called when the OK button is clicked.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onLogin() {
+ this.props.dispatch(_showLoginDialog());
+ }
+
+ /**
+ * Called when the cancel button is clicked.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onCancel() {
+ this.props.dispatch(cancelWaitForOwner());
+ }
+}
+
+/**
+ * Maps (parts of) the Redux state to the associated props for the
+ * {@code WaitForOwnerDialog} component.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {{
+ * roomName: string
+ * }}
+ */
+function _mapStateToProps(state) {
+ const {
+ authRequired
+ } = state['features/base/conference'];
+
+ return {
+ roomName: authRequired && authRequired.getName()
+ };
+}
+
+export default translate(connect(_mapStateToProps)(WaitForOwnerDialog));
diff --git a/react/features/authentication/components/index.js b/react/features/authentication/components/index.js
new file mode 100644
index 000000000..52c59fc63
--- /dev/null
+++ b/react/features/authentication/components/index.js
@@ -0,0 +1,2 @@
+export { default as LoginDialog } from './LoginDialog';
+export { default as WaitForOwnerDialog } from './WaitForOwnerDialog';
diff --git a/react/features/authentication/components/styles.js b/react/features/authentication/components/styles.js
new file mode 100644
index 000000000..26b6a5ef2
--- /dev/null
+++ b/react/features/authentication/components/styles.js
@@ -0,0 +1,23 @@
+import {
+ ColorPalette,
+ createStyleSheet
+} from '../../base/styles';
+
+/**
+ * The styles of the authentication feature.
+ */
+export default createStyleSheet({
+ outerArea: {
+ flex: 1
+ },
+ dialogBox: {
+ marginLeft: '10%',
+ marginRight: '10%',
+ marginTop: '10%',
+ backgroundColor: ColorPalette.white
+ },
+ textInput: {
+ height: 25,
+ fontSize: 16
+ }
+});
diff --git a/react/features/authentication/functions.js b/react/features/authentication/functions.js
new file mode 100644
index 000000000..f25a6a61e
--- /dev/null
+++ b/react/features/authentication/functions.js
@@ -0,0 +1,74 @@
+import {
+ LoginDialog,
+ WaitForOwnerDialog
+} from './components/index';
+import { hideDialog } from '../base/dialog/actions';
+
+/**
+ * Will clear the wait for conference owner timeout handler if any is currently
+ * set.
+ *
+ * @param {Object} store - The Redux store instance.
+ * @returns {void}
+ */
+export function clearExistingWaitForOwnerTimeout(store) {
+ const { waitForOwnerTimeoutID }
+ = store.getState()['features/authentication'];
+
+ if (waitForOwnerTimeoutID) {
+ clearTimeout(waitForOwnerTimeoutID);
+ }
+}
+
+/**
+ * Checks if {@link LoginDialog} is currently open.
+ *
+ * @param {Object|Function} getStateOrState - The Redux store instance or
+ * store's get state method.
+ * @returns {boolean}
+ */
+export function isLoginDialogOpened(getStateOrState) {
+ const state
+ = typeof getStateOrState === 'function'
+ ? getStateOrState() : getStateOrState;
+ const dialogState = state['features/base/dialog'];
+
+ return dialogState.component && dialogState.component === LoginDialog;
+}
+
+/**
+ * Hides {@link LoginDialog} if it's currently displayed.
+ *
+ * @param {Object} store - The Redux store instance.
+ * @returns {void}
+ */
+export function hideLoginDialog({ dispatch, getState }) {
+ if (isLoginDialogOpened(getState)) {
+ dispatch(hideDialog());
+ }
+}
+
+/**
+ * Checks if {@link WaitForOwnerDialog} is currently open.
+ *
+ * @param {Object} store - The Redux store instance.
+ * @returns {boolean}
+ */
+export function isWaitForOwnerDialogOpened({ getState }) {
+ const dialogState = getState()['features/base/dialog'];
+
+ return dialogState.component
+ && dialogState.component === WaitForOwnerDialog;
+}
+
+/**
+ * Checks if the cyclic "wait for conference owner" task is currently scheduled.
+ *
+ * @param {Object} store - The Redux store instance.
+ * @returns {boolean}
+ */
+export function isWaitingForOwner({ getState }) {
+ const { waitForOwnerTimeoutID } = getState()['features/authentication'];
+
+ return Boolean(waitForOwnerTimeoutID);
+}
diff --git a/react/features/authentication/index.js b/react/features/authentication/index.js
new file mode 100644
index 000000000..88b1e84c2
--- /dev/null
+++ b/react/features/authentication/index.js
@@ -0,0 +1,7 @@
+export * from './actions';
+export * from './actionTypes';
+export * from './functions';
+
+import './middleware';
+
+import './reducer';
diff --git a/react/features/authentication/middleware.js b/react/features/authentication/middleware.js
new file mode 100644
index 000000000..570a76ebc
--- /dev/null
+++ b/react/features/authentication/middleware.js
@@ -0,0 +1,136 @@
+import { MiddlewareRegistry } from '../base/redux';
+
+import {
+ clearWaitForOwnerTimeout,
+ _showLoginDialog,
+ _showWaitForOwnerDialog,
+ waitForOwner
+} from './actions';
+import { appNavigate } from '../app/actions';
+import {
+ CANCEL_LOGIN,
+ CANCEL_WAIT_FOR_OWNER,
+ STOP_WAIT_FOR_OWNER,
+ WAIT_FOR_OWNER
+} from './actionTypes';
+import {
+ CONFERENCE_FAILED,
+ CONFERENCE_JOINED,
+ CONFERENCE_LEFT
+} from '../base/conference/actionTypes';
+import { hideDialog } from '../base/dialog/actions';
+import { CONNECTION_ESTABLISHED } from '../base/connection/actionTypes';
+import { CONNECTION_FAILED } from '../base/connection';
+import {
+ clearExistingWaitForOwnerTimeout,
+ hideLoginDialog,
+ isLoginDialogOpened,
+ isWaitForOwnerDialogOpened,
+ isWaitingForOwner
+} from './functions';
+import {
+ JitsiConferenceErrors,
+ JitsiConnectionErrors
+} from '../base/lib-jitsi-meet';
+
+/**
+ * Middleware that captures connection or conference failed errors and controlls
+ * {@link WaitForOwnerDialog} and {@link LoginDialog}.
+ *
+ * FIXME Some of the complexity was introduced by the lack of dialog stacking.
+ *
+ * @param {Store} store - Redux store.
+ * @returns {Function}
+ */
+MiddlewareRegistry.register(store => next => action => {
+ switch (action.type) {
+ case CONNECTION_FAILED: {
+ if (action.error === JitsiConnectionErrors.PASSWORD_REQUIRED) {
+ store.dispatch(_showLoginDialog());
+ }
+ break;
+ }
+ case CONNECTION_ESTABLISHED: {
+ hideLoginDialog(store);
+ break;
+ }
+ case CONFERENCE_FAILED: {
+ if (action.error === JitsiConferenceErrors.AUTHENTICATION_REQUIRED) {
+ store.dispatch(waitForOwner());
+ } else {
+ store.dispatch(clearWaitForOwnerTimeout());
+ }
+ break;
+ }
+ case CONFERENCE_JOINED: {
+ if (isWaitingForOwner(store)) {
+ store.dispatch(clearWaitForOwnerTimeout());
+ }
+ hideLoginDialog(store);
+ break;
+ }
+ case CONFERENCE_LEFT: {
+ store.dispatch(clearWaitForOwnerTimeout());
+ break;
+ }
+ case WAIT_FOR_OWNER: {
+ clearExistingWaitForOwnerTimeout(store);
+ const { handler, timeoutMs } = action;
+ const newTimeoutId = setTimeout(handler, timeoutMs);
+
+ action.waitForOwnerTimeoutID = newTimeoutId;
+
+ // The WAIT_FOR_OWNER action is cyclic and we don't want to hide
+ // the login dialog every few seconds...
+ if (!isLoginDialogOpened(store.getState())) {
+ store.dispatch(_showWaitForOwnerDialog());
+ }
+ break;
+ }
+ case STOP_WAIT_FOR_OWNER: {
+ clearExistingWaitForOwnerTimeout(store);
+ if (isWaitForOwnerDialogOpened(store)) {
+ store.dispatch(hideDialog());
+ }
+ break;
+ }
+ case CANCEL_LOGIN: {
+ const { upgradeRoleInProgress }
+ = store.getState()['features/authentication'];
+
+ if (upgradeRoleInProgress) {
+ upgradeRoleInProgress.cancel();
+ }
+
+ const waitingForOwner = isWaitingForOwner(store);
+
+ // The LoginDialog can be opened on top of "wait for owner". The app
+ // should navigate only if LoginDialog was open without
+ // the WaitForOwnerDialog.
+ if (!isWaitForOwnerDialogOpened(store) && !waitingForOwner) {
+ // Go back to app entry point
+ hideLoginDialog(store);
+ store.dispatch(appNavigate(undefined));
+ } else if (!isWaitForOwnerDialogOpened(store) && waitingForOwner) {
+ // Instead of hiding show the new one.
+ const result = next(action);
+
+ store.dispatch(_showWaitForOwnerDialog());
+
+ return result;
+ }
+ break;
+ }
+ case CANCEL_WAIT_FOR_OWNER: {
+ const result = next(action);
+
+ store.dispatch(clearWaitForOwnerTimeout());
+ store.dispatch(appNavigate(undefined));
+
+ return result;
+ }
+
+ }
+
+ return next(action);
+});
diff --git a/react/features/authentication/reducer.js b/react/features/authentication/reducer.js
new file mode 100644
index 000000000..f54a34c93
--- /dev/null
+++ b/react/features/authentication/reducer.js
@@ -0,0 +1,45 @@
+import { assign } from '../base/redux/functions';
+import { ReducerRegistry } from '../base/redux';
+
+import {
+ CANCEL_LOGIN,
+ STOP_WAIT_FOR_OWNER,
+ UPGRADE_ROLE_FAILED, UPGRADE_ROLE_STARTED, UPGRADE_ROLE_SUCCESS,
+ WAIT_FOR_OWNER
+} from './actionTypes';
+
+ReducerRegistry.register('features/authentication', (state = { }, action) => {
+ switch (action.type) {
+ case WAIT_FOR_OWNER:
+ return assign(state, {
+ waitForOwnerTimeoutID: action.waitForOwnerTimeoutID
+ });
+ case UPGRADE_ROLE_STARTED:
+ return assign(state, {
+ upgradeRoleError: undefined,
+ upgradeRoleInProgress: action.authConnection
+ });
+ case UPGRADE_ROLE_SUCCESS:
+ return assign(state, {
+ upgradeRoleError: undefined,
+ upgradeRoleInProgress: undefined
+ });
+ case UPGRADE_ROLE_FAILED:
+ return assign(state, {
+ upgradeRoleError: action.error,
+ upgradeRoleInProgress: undefined
+ });
+ case CANCEL_LOGIN:
+ return assign(state, {
+ upgradeRoleError: undefined,
+ upgradeRoleInProgress: undefined
+ });
+ case STOP_WAIT_FOR_OWNER:
+ return assign(state, {
+ waitForOwnerTimeoutID: undefined,
+ upgradeRoleError: undefined
+ });
+ }
+
+ return state;
+});
diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js
index fdd9a006d..8ddf2cc47 100644
--- a/react/features/base/conference/actions.js
+++ b/react/features/base/conference/actions.js
@@ -288,6 +288,25 @@ export function createConference() {
};
}
+/**
+ * Will try to join the conference again in case it failed earlier with
+ * {@link JitsiConferenceErrors.AUTHENTICATION_REQUIRED}. It means that Jicofo
+ * did not allow to create new room from anonymous domain, but it can be tried
+ * again later in case authenticated user created it in the meantime.
+ *
+ * @returns {Function}
+ */
+export function checkIfCanJoin() {
+ return (dispatch, getState) => {
+ const { password, authRequired }
+ = getState()['features/base/conference'];
+
+ if (authRequired) {
+ authRequired.join(password);
+ }
+ };
+}
+
/**
* Signals the data channel with the bridge has successfully opened.
*
diff --git a/react/features/base/conference/reducer.js b/react/features/base/conference/reducer.js
index cda59a88d..de2dbfa20 100644
--- a/react/features/base/conference/reducer.js
+++ b/react/features/base/conference/reducer.js
@@ -84,7 +84,13 @@ function _conferenceFailed(state, { conference, error }) {
? conference
: undefined;
+ const authRequired
+ = JitsiConferenceErrors.AUTHENTICATION_REQUIRED === error
+ ? conference
+ : undefined;
+
return assign(state, {
+ authRequired,
conference: undefined,
joining: undefined,
leaving: undefined,
@@ -126,6 +132,8 @@ function _conferenceJoined(state, { conference }) {
const locked = conference.room.locked ? LOCKED_REMOTELY : undefined;
return assign(state, {
+ authRequired: undefined,
+
/**
* The JitsiConference instance represented by the Redux state of the
* feature base/conference.
@@ -170,6 +178,7 @@ function _conferenceLeft(state, { conference }) {
}
return assign(state, {
+ authRequired: undefined,
conference: undefined,
joining: undefined,
leaving: undefined,
@@ -209,6 +218,7 @@ function _conferenceWillLeave(state, { conference }) {
}
return assign(state, {
+ authRequired: undefined,
joining: undefined,
/**
diff --git a/react/features/base/connection/actions.native.js b/react/features/base/connection/actions.native.js
index 0a60a8257..1b078e80b 100644
--- a/react/features/base/connection/actions.native.js
+++ b/react/features/base/connection/actions.native.js
@@ -18,9 +18,12 @@ import {
/**
* Opens new connection.
*
+ * @param {string} [username] - The XMPP user id eg. user@server.com.
+ * @param {string} [password] - The user's password.
+ *
* @returns {Function}
*/
-export function connect() {
+export function connect(username: ?string, password: ?string) {
return (dispatch: Dispatch<*>, getState: Function) => {
const state = getState();
const options = _constructOptions(state);
@@ -43,7 +46,10 @@ export function connect() {
JitsiConnectionEvents.CONNECTION_FAILED,
_onConnectionFailed);
- connection.connect();
+ connection.connect({
+ id: username,
+ password
+ });
/**
* Dispatches CONNECTION_DISCONNECTED action when connection is
diff --git a/react/features/base/connection/functions.js b/react/features/base/connection/functions.js
index f9c60ceca..458cfbd75 100644
--- a/react/features/base/connection/functions.js
+++ b/react/features/base/connection/functions.js
@@ -51,3 +51,26 @@ export function getURLWithoutParams(url: URL): URL {
return url;
}
+
+/**
+ * Convert provided id to jid if it's not jid yet.
+ *
+ * @param {string} id - User id or jid.
+ * @param {Object} configHosts - The 'hosts' part of the config object.
+ * @returns {string} jid - A string in the form of user@server.com.
+ */
+export function toJid(id: string, configHosts: Object): string {
+ if (id.indexOf('@') >= 0) {
+ return id;
+ }
+
+ let jid = id.concat('@');
+
+ if (configHosts.authdomain) {
+ jid += configHosts.authdomain;
+ } else {
+ jid += configHosts.domain;
+ }
+
+ return jid;
+}