From ea56010e09efb20839fff026e9f357864f206c7c Mon Sep 17 00:00:00 2001 From: Avram Tudor Date: Fri, 25 Jun 2021 16:28:54 +0300 Subject: [PATCH] =?UTF-8?q?feat(jaas)=20display=20messages=20about=20featu?= =?UTF-8?q?res=20that=20are=20disabled=20for=20jaas=E2=80=A6=20(#9448)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(jaas) display messages about features that are disabled for jaas users * code review --- css/_plan-limit.scss | 0 css/main.scss | 1 + lang/main.json | 3 + react/features/app/middlewares.any.js | 1 + react/features/app/reducers.any.js | 1 + react/features/jaas/actionTypes.js | 4 ++ react/features/jaas/actions.js | 72 +++++++++++++++++++ react/features/jaas/components/index.web.js | 3 + .../components/web/PremiumFeatureDialog.js | 62 ++++++++++++++++ react/features/jaas/components/web/index.js | 3 + react/features/jaas/constants.js | 25 +++++++ react/features/jaas/functions.js | 48 +++++++++++++ react/features/jaas/logger.js | 5 ++ react/features/jaas/middleware.web.js | 33 +++++++++ react/features/jaas/reducer.js | 28 ++++++++ .../LiveStream/AbstractLiveStreamButton.js | 14 ++-- .../Recording/AbstractRecordButton.js | 14 ++-- .../components/AbstractClosedCaptionButton.js | 11 ++- static/planLimit.html | 0 19 files changed, 318 insertions(+), 10 deletions(-) create mode 100644 css/_plan-limit.scss create mode 100644 react/features/jaas/actionTypes.js create mode 100644 react/features/jaas/actions.js create mode 100644 react/features/jaas/components/index.web.js create mode 100644 react/features/jaas/components/web/PremiumFeatureDialog.js create mode 100644 react/features/jaas/components/web/index.js create mode 100644 react/features/jaas/constants.js create mode 100644 react/features/jaas/functions.js create mode 100644 react/features/jaas/logger.js create mode 100644 react/features/jaas/middleware.web.js create mode 100644 react/features/jaas/reducer.js create mode 100644 static/planLimit.html diff --git a/css/_plan-limit.scss b/css/_plan-limit.scss new file mode 100644 index 000000000..e69de29bb diff --git a/css/main.scss b/css/main.scss index 95bfa6f69..b9ab729cf 100644 --- a/css/main.scss +++ b/css/main.scss @@ -104,5 +104,6 @@ $flagsImagePath: "../images/"; @import 'connection-status'; @import 'drawer'; @import 'participants-pane'; +@import 'plan-limit'; /* Modules END */ diff --git a/lang/main.json b/lang/main.json index ab8f3d331..2f5b132ae 100644 --- a/lang/main.json +++ b/lang/main.json @@ -323,6 +323,9 @@ "userIdentifier": "User identifier", "userPassword": "User password", "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 {{room}} has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.", "WaitForHostMsgWOk": "The conference {{room}} 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 ...", diff --git a/react/features/app/middlewares.any.js b/react/features/app/middlewares.any.js index 95fa4e4a4..e9e941001 100644 --- a/react/features/app/middlewares.any.js +++ b/react/features/app/middlewares.any.js @@ -30,6 +30,7 @@ import '../etherpad/middleware'; import '../filmstrip/middleware'; import '../follow-me/middleware'; import '../invite/middleware'; +import '../jaas/middleware'; import '../large-video/middleware'; import '../lobby/middleware'; import '../notifications/middleware'; diff --git a/react/features/app/reducers.any.js b/react/features/app/reducers.any.js index 060dd4e3b..3e06590db 100644 --- a/react/features/app/reducers.any.js +++ b/react/features/app/reducers.any.js @@ -36,6 +36,7 @@ import '../filmstrip/reducer'; import '../follow-me/reducer'; import '../google-api/reducer'; import '../invite/reducer'; +import '../jaas/reducer'; import '../large-video/reducer'; import '../lobby/reducer'; import '../notifications/reducer'; diff --git a/react/features/jaas/actionTypes.js b/react/features/jaas/actionTypes.js new file mode 100644 index 000000000..d00a8d01a --- /dev/null +++ b/react/features/jaas/actionTypes.js @@ -0,0 +1,4 @@ +/** + * Action used to store jaas customer details + */ +export const SET_DETAILS = 'SET_DETAILS'; diff --git a/react/features/jaas/actions.js b/react/features/jaas/actions.js new file mode 100644 index 000000000..058743ea8 --- /dev/null +++ b/react/features/jaas/actions.js @@ -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; + }; +} diff --git a/react/features/jaas/components/index.web.js b/react/features/jaas/components/index.web.js new file mode 100644 index 000000000..40d5f4652 --- /dev/null +++ b/react/features/jaas/components/index.web.js @@ -0,0 +1,3 @@ +// @flow + +export * from './web'; diff --git a/react/features/jaas/components/web/PremiumFeatureDialog.js b/react/features/jaas/components/web/PremiumFeatureDialog.js new file mode 100644 index 000000000..761fa7ba1 --- /dev/null +++ b/react/features/jaas/components/web/PremiumFeatureDialog.js @@ -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} + */ +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 ( + + {t('dialog.viewUpgradeOptionsContent')} + + ); + } +} + +export default translate(PremiumFeatureDialog); diff --git a/react/features/jaas/components/web/index.js b/react/features/jaas/components/web/index.js new file mode 100644 index 000000000..4ca5355d7 --- /dev/null +++ b/react/features/jaas/components/web/index.js @@ -0,0 +1,3 @@ +// @flow + +export { default as PremiumFeatureDialog } from './PremiumFeatureDialog'; diff --git a/react/features/jaas/constants.js b/react/features/jaas/constants.js new file mode 100644 index 000000000..c312ede65 --- /dev/null +++ b/react/features/jaas/constants.js @@ -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'; diff --git a/react/features/jaas/functions.js b/react/features/jaas/functions.js new file mode 100644 index 000000000..627b7b1a0 --- /dev/null +++ b/react/features/jaas/functions.js @@ -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); +} diff --git a/react/features/jaas/logger.js b/react/features/jaas/logger.js new file mode 100644 index 000000000..725fa328d --- /dev/null +++ b/react/features/jaas/logger.js @@ -0,0 +1,5 @@ +// @flow + +import { getLogger } from '../base/logging/functions'; + +export default getLogger('features/jaas'); diff --git a/react/features/jaas/middleware.web.js b/react/features/jaas/middleware.web.js new file mode 100644 index 000000000..0e7cf83a0 --- /dev/null +++ b/react/features/jaas/middleware.web.js @@ -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); +}); diff --git a/react/features/jaas/reducer.js b/react/features/jaas/reducer.js new file mode 100644 index 000000000..c6c36ea89 --- /dev/null +++ b/react/features/jaas/reducer.js @@ -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; + } + }, +); diff --git a/react/features/recording/components/LiveStream/AbstractLiveStreamButton.js b/react/features/recording/components/LiveStream/AbstractLiveStreamButton.js index 0eb8b3cae..d0934497a 100644 --- a/react/features/recording/components/LiveStream/AbstractLiveStreamButton.js +++ b/react/features/recording/components/LiveStream/AbstractLiveStreamButton.js @@ -8,6 +8,8 @@ import { isLocalParticipantModerator } from '../../../base/participants'; import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; +import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions'; +import { FEATURES } from '../../../jaas/constants'; import { getActiveSession } from '../../functions'; import { @@ -73,12 +75,16 @@ export default class AbstractLiveStreamButton extends AbstractButton

extends AbstractButton * @protected * @returns {void} */ - _handleClick() { + async _handleClick() { const { _isRecordingRunning, dispatch } = this.props; sendAnalytics(createToolbarEvent( @@ -84,9 +86,13 @@ export default class AbstractRecordButton extends AbstractButton type: JitsiRecordingConstants.mode.FILE })); - dispatch(openDialog( - _isRecordingRunning ? StopRecordingDialog : StartRecordingDialog - )); + const dialogShown = await dispatch(maybeShowPremiumFeatureDialog(FEATURES.RECORDING)); + + if (!dialogShown) { + dispatch(openDialog( + _isRecordingRunning ? StopRecordingDialog : StartRecordingDialog + )); + } } /** diff --git a/react/features/subtitles/components/AbstractClosedCaptionButton.js b/react/features/subtitles/components/AbstractClosedCaptionButton.js index c61c5fbef..0d1f2f867 100644 --- a/react/features/subtitles/components/AbstractClosedCaptionButton.js +++ b/react/features/subtitles/components/AbstractClosedCaptionButton.js @@ -3,6 +3,8 @@ import { createToolbarEvent, sendAnalytics } from '../../analytics'; import { isLocalParticipantModerator } from '../../base/participants'; import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; +import { maybeShowPremiumFeatureDialog } from '../../jaas/actions'; +import { FEATURES } from '../../jaas/constants'; import { toggleRequestingSubtitles } from '../actions'; export type AbstractProps = AbstractButtonProps & { @@ -35,7 +37,7 @@ export class AbstractClosedCaptionButton * @protected * @returns {void} */ - _handleClick() { + async _handleClick() { const { _requestingSubtitles, dispatch } = this.props; sendAnalytics(createToolbarEvent('transcribing.ccButton', @@ -43,7 +45,12 @@ export class AbstractClosedCaptionButton 'requesting_subtitles': Boolean(_requestingSubtitles) })); - dispatch(toggleRequestingSubtitles()); + + const dialogShown = await dispatch(maybeShowPremiumFeatureDialog(FEATURES.RECORDING)); + + if (!dialogShown) { + dispatch(toggleRequestingSubtitles()); + } } /** diff --git a/static/planLimit.html b/static/planLimit.html new file mode 100644 index 000000000..e69de29bb