feat(jaas) display messages about features that are disabled for jaas… (#9448)
* feat(jaas) display messages about features that are disabled for jaas users * code review
This commit is contained in:
parent
38b14c5d62
commit
ea56010e09
|
@ -104,5 +104,6 @@ $flagsImagePath: "../images/";
|
||||||
@import 'connection-status';
|
@import 'connection-status';
|
||||||
@import 'drawer';
|
@import 'drawer';
|
||||||
@import 'participants-pane';
|
@import 'participants-pane';
|
||||||
|
@import 'plan-limit';
|
||||||
|
|
||||||
/* Modules END */
|
/* Modules END */
|
||||||
|
|
|
@ -323,6 +323,9 @@
|
||||||
"userIdentifier": "User identifier",
|
"userIdentifier": "User identifier",
|
||||||
"userPassword": "User password",
|
"userPassword": "User password",
|
||||||
"videoLink": "Video link",
|
"videoLink": "Video link",
|
||||||
|
"viewUpgradeOptions": "View upgrade options",
|
||||||
|
"viewUpgradeOptionsContent": "To get unlimited access to premium features like recording, transcriptions, RTMP Streaming & more, you'll need to upgrade your plan.",
|
||||||
|
"viewUpgradeOptionsTitle": "You discovered a premium feature!",
|
||||||
"WaitForHostMsg": "The conference <b>{{room}}</b> has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
|
"WaitForHostMsg": "The conference <b>{{room}}</b> has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
|
||||||
"WaitForHostMsgWOk": "The conference <b>{{room}}</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.",
|
"WaitForHostMsgWOk": "The conference <b>{{room}}</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.",
|
||||||
"WaitingForHostTitle": "Waiting for the host ...",
|
"WaitingForHostTitle": "Waiting for the host ...",
|
||||||
|
|
|
@ -30,6 +30,7 @@ import '../etherpad/middleware';
|
||||||
import '../filmstrip/middleware';
|
import '../filmstrip/middleware';
|
||||||
import '../follow-me/middleware';
|
import '../follow-me/middleware';
|
||||||
import '../invite/middleware';
|
import '../invite/middleware';
|
||||||
|
import '../jaas/middleware';
|
||||||
import '../large-video/middleware';
|
import '../large-video/middleware';
|
||||||
import '../lobby/middleware';
|
import '../lobby/middleware';
|
||||||
import '../notifications/middleware';
|
import '../notifications/middleware';
|
||||||
|
|
|
@ -36,6 +36,7 @@ import '../filmstrip/reducer';
|
||||||
import '../follow-me/reducer';
|
import '../follow-me/reducer';
|
||||||
import '../google-api/reducer';
|
import '../google-api/reducer';
|
||||||
import '../invite/reducer';
|
import '../invite/reducer';
|
||||||
|
import '../jaas/reducer';
|
||||||
import '../large-video/reducer';
|
import '../large-video/reducer';
|
||||||
import '../lobby/reducer';
|
import '../lobby/reducer';
|
||||||
import '../notifications/reducer';
|
import '../notifications/reducer';
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Action used to store jaas customer details
|
||||||
|
*/
|
||||||
|
export const SET_DETAILS = 'SET_DETAILS';
|
|
@ -0,0 +1,72 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { openDialog } from '../base/dialog';
|
||||||
|
import { getVpaasTenant } from '../billing-counter/functions';
|
||||||
|
|
||||||
|
import { SET_DETAILS } from './actionTypes';
|
||||||
|
import { PremiumFeatureDialog } from './components';
|
||||||
|
import { isFeatureDisabled, sendGetDetailsRequest } from './functions';
|
||||||
|
import logger from './logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action used to set the jaas customer details in store.
|
||||||
|
*
|
||||||
|
* @param {Object} details - The customer details object.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
function setCustomerDetails(details) {
|
||||||
|
return {
|
||||||
|
type: SET_DETAILS,
|
||||||
|
payload: details
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a request for retrieving jaas customer details.
|
||||||
|
*
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function getCustomerDetails() {
|
||||||
|
return async function(dispatch: Function, getState: Function) {
|
||||||
|
const state = getState();
|
||||||
|
const baseUrl = state['features/base/config'].jaasActuatorUrl;
|
||||||
|
const jwt = state['features/base/jwt'].jwt;
|
||||||
|
const appId = getVpaasTenant(state);
|
||||||
|
|
||||||
|
const shouldSendRequest = Boolean(baseUrl && jwt && appId);
|
||||||
|
|
||||||
|
if (shouldSendRequest) {
|
||||||
|
try {
|
||||||
|
const details = await sendGetDetailsRequest({
|
||||||
|
baseUrl,
|
||||||
|
jwt,
|
||||||
|
appId
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(setCustomerDetails(details));
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Could not send request', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a dialog prompting users to upgrade, if requested feature is disabled.
|
||||||
|
*
|
||||||
|
* @param {string} feature - The feature to check availability for.
|
||||||
|
*
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function maybeShowPremiumFeatureDialog(feature: string) {
|
||||||
|
return function(dispatch: Function, getState: Function) {
|
||||||
|
if (isFeatureDisabled(getState(), feature)) {
|
||||||
|
dispatch(openDialog(PremiumFeatureDialog));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
export * from './web';
|
|
@ -0,0 +1,62 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
import { Dialog } from '../../../base/dialog';
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { openURLInBrowser } from '../../../base/util';
|
||||||
|
import { JAAS_UPGRADE_URL } from '../../constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders the premium feature dialog.
|
||||||
|
*
|
||||||
|
* @returns {React$Element<any>}
|
||||||
|
*/
|
||||||
|
class PremiumFeatureDialog extends PureComponent<*> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new component.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._onSubmitValue = this._onSubmitValue.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_onSubmitValue: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked when the dialog ok is pressed.
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_onSubmitValue() {
|
||||||
|
openURLInBrowser(JAAS_UPGRADE_URL, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
hideCancelButton = { true }
|
||||||
|
okKey = { t('dialog.viewUpgradeOptions') }
|
||||||
|
onSubmit = { this._onSubmitValue }
|
||||||
|
titleKey = { t('dialog.viewUpgradeOptionsTitle') }
|
||||||
|
width = { 'small' }>
|
||||||
|
<span>{t('dialog.viewUpgradeOptionsContent')}</span>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(PremiumFeatureDialog);
|
|
@ -0,0 +1,3 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
export { default as PremiumFeatureDialog } from './PremiumFeatureDialog';
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* JaaS customer statuses which represent their account state
|
||||||
|
*/
|
||||||
|
export const STATUSES = {
|
||||||
|
ACTIVE: 'ACTIVE',
|
||||||
|
BLOCKED: 'BLOCKED'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service features for JaaS users
|
||||||
|
*/
|
||||||
|
export const FEATURES = {
|
||||||
|
INBOUND_CALL: 'inbound-call',
|
||||||
|
OUTBOUND_CALL: 'outbound-call',
|
||||||
|
RECORDING: 'recording',
|
||||||
|
SIP_INBOUND_CALL: 'sip-inbound-call',
|
||||||
|
SIP_OUTBOUND_CALL: 'sip-outbound-call',
|
||||||
|
STREAMING: 'streaming',
|
||||||
|
TRANSCRIPTION: 'transcription'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL for displaying JaaS upgrade options
|
||||||
|
*/
|
||||||
|
export const JAAS_UPGRADE_URL = 'https://jaas.8x8.vc/#/plan/upgrade';
|
|
@ -0,0 +1,48 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a request for retrieving jaas customer details.
|
||||||
|
*
|
||||||
|
* @param {Object} reqData - The request info.
|
||||||
|
* @param {string} reqData.appId - The client appId.
|
||||||
|
* @param {string} reqData.baseUrl - The base url for the request.
|
||||||
|
* @param {string} reqData.jwt - The JWT token.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export async function sendGetDetailsRequest({ appId, baseUrl, jwt }: {
|
||||||
|
appId: string,
|
||||||
|
baseUrl: string,
|
||||||
|
jwt: string,
|
||||||
|
}) {
|
||||||
|
const fullUrl = `${baseUrl}/v1/customers/${encodeURIComponent(appId)}`;
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${jwt}`
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(fullUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Request not successful');
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(err);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the billing id for vpaas meetings.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The state of the app.
|
||||||
|
* @param {string} feature - Feature to be looked up for disable state.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isFeatureDisabled(state: Object, feature: string) {
|
||||||
|
return state['features/jaas'].disabledFeatures.includes(feature);
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { getLogger } from '../base/logging/functions';
|
||||||
|
|
||||||
|
export default getLogger('features/jaas');
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { redirectToStaticPage } from '../app/actions';
|
||||||
|
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
|
||||||
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
|
|
||||||
|
import { SET_DETAILS } from './actionTypes';
|
||||||
|
import { getCustomerDetails } from './actions';
|
||||||
|
import { STATUSES } from './constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The redux middleware for billing counter.
|
||||||
|
*
|
||||||
|
* @param {Store} store - The redux store.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
MiddlewareRegistry.register(store => next => async action => {
|
||||||
|
switch (action.type) {
|
||||||
|
case CONFERENCE_JOINED: {
|
||||||
|
store.dispatch(getCustomerDetails());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SET_DETAILS: {
|
||||||
|
const { status } = action.payload;
|
||||||
|
|
||||||
|
if (status === STATUSES.BLOCKED) {
|
||||||
|
store.dispatch(redirectToStaticPage('/static/planLimit.html'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
});
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { ReducerRegistry } from '../base/redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SET_DETAILS
|
||||||
|
} from './actionTypes';
|
||||||
|
import { STATUSES } from './constants';
|
||||||
|
|
||||||
|
const DEFAULT_STATE = {
|
||||||
|
disabledFeatures: [],
|
||||||
|
status: STATUSES.ACTIVE
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for actions that mutate the billing-counter state
|
||||||
|
*/
|
||||||
|
ReducerRegistry.register(
|
||||||
|
'features/jaas', (state = DEFAULT_STATE, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case SET_DETAILS: {
|
||||||
|
return action.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
|
@ -8,6 +8,8 @@ import {
|
||||||
isLocalParticipantModerator
|
isLocalParticipantModerator
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
||||||
|
import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
|
||||||
|
import { FEATURES } from '../../../jaas/constants';
|
||||||
import { getActiveSession } from '../../functions';
|
import { getActiveSession } from '../../functions';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -73,12 +75,16 @@ export default class AbstractLiveStreamButton<P: Props> extends AbstractButton<P
|
||||||
* @protected
|
* @protected
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_handleClick() {
|
async _handleClick() {
|
||||||
const { _isLiveStreamRunning, dispatch } = this.props;
|
const { _isLiveStreamRunning, dispatch } = this.props;
|
||||||
|
|
||||||
dispatch(openDialog(
|
const dialogShown = await dispatch(maybeShowPremiumFeatureDialog(FEATURES.RECORDING));
|
||||||
_isLiveStreamRunning ? StopLiveStreamDialog : StartLiveStreamDialog
|
|
||||||
));
|
if (!dialogShown) {
|
||||||
|
dispatch(openDialog(
|
||||||
|
_isLiveStreamRunning ? StopLiveStreamDialog : StartLiveStreamDialog
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -12,6 +12,8 @@ import {
|
||||||
isLocalParticipantModerator
|
isLocalParticipantModerator
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
||||||
|
import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
|
||||||
|
import { FEATURES } from '../../../jaas/constants';
|
||||||
import { getActiveSession } from '../../functions';
|
import { getActiveSession } from '../../functions';
|
||||||
|
|
||||||
import { StartRecordingDialog, StopRecordingDialog } from './_';
|
import { StartRecordingDialog, StopRecordingDialog } from './_';
|
||||||
|
@ -74,7 +76,7 @@ export default class AbstractRecordButton<P: Props> extends AbstractButton<P, *>
|
||||||
* @protected
|
* @protected
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_handleClick() {
|
async _handleClick() {
|
||||||
const { _isRecordingRunning, dispatch } = this.props;
|
const { _isRecordingRunning, dispatch } = this.props;
|
||||||
|
|
||||||
sendAnalytics(createToolbarEvent(
|
sendAnalytics(createToolbarEvent(
|
||||||
|
@ -84,9 +86,13 @@ export default class AbstractRecordButton<P: Props> extends AbstractButton<P, *>
|
||||||
type: JitsiRecordingConstants.mode.FILE
|
type: JitsiRecordingConstants.mode.FILE
|
||||||
}));
|
}));
|
||||||
|
|
||||||
dispatch(openDialog(
|
const dialogShown = await dispatch(maybeShowPremiumFeatureDialog(FEATURES.RECORDING));
|
||||||
_isRecordingRunning ? StopRecordingDialog : StartRecordingDialog
|
|
||||||
));
|
if (!dialogShown) {
|
||||||
|
dispatch(openDialog(
|
||||||
|
_isRecordingRunning ? StopRecordingDialog : StartRecordingDialog
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
import { createToolbarEvent, sendAnalytics } from '../../analytics';
|
import { createToolbarEvent, sendAnalytics } from '../../analytics';
|
||||||
import { isLocalParticipantModerator } from '../../base/participants';
|
import { isLocalParticipantModerator } from '../../base/participants';
|
||||||
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
||||||
|
import { maybeShowPremiumFeatureDialog } from '../../jaas/actions';
|
||||||
|
import { FEATURES } from '../../jaas/constants';
|
||||||
import { toggleRequestingSubtitles } from '../actions';
|
import { toggleRequestingSubtitles } from '../actions';
|
||||||
|
|
||||||
export type AbstractProps = AbstractButtonProps & {
|
export type AbstractProps = AbstractButtonProps & {
|
||||||
|
@ -35,7 +37,7 @@ export class AbstractClosedCaptionButton
|
||||||
* @protected
|
* @protected
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_handleClick() {
|
async _handleClick() {
|
||||||
const { _requestingSubtitles, dispatch } = this.props;
|
const { _requestingSubtitles, dispatch } = this.props;
|
||||||
|
|
||||||
sendAnalytics(createToolbarEvent('transcribing.ccButton',
|
sendAnalytics(createToolbarEvent('transcribing.ccButton',
|
||||||
|
@ -43,7 +45,12 @@ export class AbstractClosedCaptionButton
|
||||||
'requesting_subtitles': Boolean(_requestingSubtitles)
|
'requesting_subtitles': Boolean(_requestingSubtitles)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
dispatch(toggleRequestingSubtitles());
|
|
||||||
|
const dialogShown = await dispatch(maybeShowPremiumFeatureDialog(FEATURES.RECORDING));
|
||||||
|
|
||||||
|
if (!dialogShown) {
|
||||||
|
dispatch(toggleRequestingSubtitles());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue