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:
Avram Tudor 2021-06-25 16:28:54 +03:00 committed by GitHub
parent 38b14c5d62
commit ea56010e09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 318 additions and 10 deletions

0
css/_plan-limit.scss Normal file
View File

View File

@ -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 */

View File

@ -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 ...",

View File

@ -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';

View File

@ -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';

View File

@ -0,0 +1,4 @@
/**
* Action used to store jaas customer details
*/
export const SET_DETAILS = 'SET_DETAILS';

View File

@ -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;
};
}

View File

@ -0,0 +1,3 @@
// @flow
export * from './web';

View File

@ -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);

View File

@ -0,0 +1,3 @@
// @flow
export { default as PremiumFeatureDialog } from './PremiumFeatureDialog';

View File

@ -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';

View File

@ -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);
}

View File

@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../base/logging/functions';
export default getLogger('features/jaas');

View File

@ -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);
});

View File

@ -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;
}
},
);

View File

@ -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
));
}
} }
/** /**

View File

@ -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
));
}
} }
/** /**

View File

@ -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());
}
} }
/** /**

0
static/planLimit.html Normal file
View File