Browse Source

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
j8
Avram Tudor 3 years ago
parent
commit
ea56010e09
No account linked to committer's email address

+ 0
- 0
css/_plan-limit.scss View File


+ 1
- 0
css/main.scss View File

@@ -104,5 +104,6 @@ $flagsImagePath: "../images/";
104 104
 @import 'connection-status';
105 105
 @import 'drawer';
106 106
 @import 'participants-pane';
107
+@import 'plan-limit';
107 108
 
108 109
 /* Modules END */

+ 3
- 0
lang/main.json View File

@@ -323,6 +323,9 @@
323 323
         "userIdentifier": "User identifier",
324 324
         "userPassword": "User password",
325 325
         "videoLink": "Video link",
326
+        "viewUpgradeOptions": "View upgrade options",
327
+        "viewUpgradeOptionsContent": "To get unlimited access to premium features like recording, transcriptions, RTMP Streaming & more, you'll need to upgrade your plan.",
328
+        "viewUpgradeOptionsTitle": "You discovered a premium feature!",
326 329
         "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.",
327 330
         "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.",
328 331
         "WaitingForHostTitle": "Waiting for the host ...",

+ 1
- 0
react/features/app/middlewares.any.js View File

@@ -30,6 +30,7 @@ import '../etherpad/middleware';
30 30
 import '../filmstrip/middleware';
31 31
 import '../follow-me/middleware';
32 32
 import '../invite/middleware';
33
+import '../jaas/middleware';
33 34
 import '../large-video/middleware';
34 35
 import '../lobby/middleware';
35 36
 import '../notifications/middleware';

+ 1
- 0
react/features/app/reducers.any.js View File

@@ -36,6 +36,7 @@ import '../filmstrip/reducer';
36 36
 import '../follow-me/reducer';
37 37
 import '../google-api/reducer';
38 38
 import '../invite/reducer';
39
+import '../jaas/reducer';
39 40
 import '../large-video/reducer';
40 41
 import '../lobby/reducer';
41 42
 import '../notifications/reducer';

+ 4
- 0
react/features/jaas/actionTypes.js View File

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

+ 72
- 0
react/features/jaas/actions.js View File

@@ -0,0 +1,72 @@
1
+// @flow
2
+
3
+import { openDialog } from '../base/dialog';
4
+import { getVpaasTenant } from '../billing-counter/functions';
5
+
6
+import { SET_DETAILS } from './actionTypes';
7
+import { PremiumFeatureDialog } from './components';
8
+import { isFeatureDisabled, sendGetDetailsRequest } from './functions';
9
+import logger from './logger';
10
+
11
+/**
12
+ * Action used to set the jaas customer details in store.
13
+ *
14
+ * @param {Object} details - The customer details object.
15
+ * @returns {Object}
16
+ */
17
+function setCustomerDetails(details) {
18
+    return {
19
+        type: SET_DETAILS,
20
+        payload: details
21
+    };
22
+}
23
+
24
+/**
25
+ * Sends a request for retrieving jaas customer details.
26
+ *
27
+ * @returns {Function}
28
+ */
29
+export function getCustomerDetails() {
30
+    return async function(dispatch: Function, getState: Function) {
31
+        const state = getState();
32
+        const baseUrl = state['features/base/config'].jaasActuatorUrl;
33
+        const jwt = state['features/base/jwt'].jwt;
34
+        const appId = getVpaasTenant(state);
35
+
36
+        const shouldSendRequest = Boolean(baseUrl && jwt && appId);
37
+
38
+        if (shouldSendRequest) {
39
+            try {
40
+                const details = await sendGetDetailsRequest({
41
+                    baseUrl,
42
+                    jwt,
43
+                    appId
44
+                });
45
+
46
+                dispatch(setCustomerDetails(details));
47
+            } catch (err) {
48
+                logger.error('Could not send request', err);
49
+            }
50
+        }
51
+    };
52
+}
53
+
54
+
55
+/**
56
+ * Shows a dialog prompting users to upgrade, if requested feature is disabled.
57
+ *
58
+ * @param {string} feature - The feature to check availability for.
59
+ *
60
+ * @returns {Function}
61
+ */
62
+export function maybeShowPremiumFeatureDialog(feature: string) {
63
+    return function(dispatch: Function, getState: Function) {
64
+        if (isFeatureDisabled(getState(), feature)) {
65
+            dispatch(openDialog(PremiumFeatureDialog));
66
+
67
+            return true;
68
+        }
69
+
70
+        return false;
71
+    };
72
+}

+ 3
- 0
react/features/jaas/components/index.web.js View File

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

+ 62
- 0
react/features/jaas/components/web/PremiumFeatureDialog.js View File

@@ -0,0 +1,62 @@
1
+// @flow
2
+
3
+import React, { PureComponent } from 'react';
4
+
5
+
6
+import { Dialog } from '../../../base/dialog';
7
+import { translate } from '../../../base/i18n';
8
+import { openURLInBrowser } from '../../../base/util';
9
+import { JAAS_UPGRADE_URL } from '../../constants';
10
+
11
+/**
12
+ * Component that renders the premium feature dialog.
13
+ *
14
+ * @returns {React$Element<any>}
15
+ */
16
+class PremiumFeatureDialog extends PureComponent<*> {
17
+
18
+    /**
19
+     * Instantiates a new component.
20
+     *
21
+     * @inheritdoc
22
+     */
23
+    constructor(props) {
24
+        super(props);
25
+
26
+        this._onSubmitValue = this._onSubmitValue.bind(this);
27
+    }
28
+
29
+
30
+    _onSubmitValue: () => void;
31
+
32
+    /**
33
+     * Callback to be invoked when the dialog ok is pressed.
34
+     *
35
+     * @returns {boolean}
36
+     */
37
+    _onSubmitValue() {
38
+        openURLInBrowser(JAAS_UPGRADE_URL, true);
39
+    }
40
+
41
+    /**
42
+     * Implements React's {@link Component#render()}.
43
+     *
44
+     * @inheritdoc
45
+     */
46
+    render() {
47
+        const { t } = this.props;
48
+
49
+        return (
50
+            <Dialog
51
+                hideCancelButton = { true }
52
+                okKey = { t('dialog.viewUpgradeOptions') }
53
+                onSubmit = { this._onSubmitValue }
54
+                titleKey = { t('dialog.viewUpgradeOptionsTitle') }
55
+                width = { 'small' }>
56
+                <span>{t('dialog.viewUpgradeOptionsContent')}</span>
57
+            </Dialog>
58
+        );
59
+    }
60
+}
61
+
62
+export default translate(PremiumFeatureDialog);

+ 3
- 0
react/features/jaas/components/web/index.js View File

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

+ 25
- 0
react/features/jaas/constants.js View File

@@ -0,0 +1,25 @@
1
+/**
2
+ * JaaS customer statuses which represent their account state
3
+ */
4
+export const STATUSES = {
5
+    ACTIVE: 'ACTIVE',
6
+    BLOCKED: 'BLOCKED'
7
+};
8
+
9
+/**
10
+ * Service features for JaaS users
11
+ */
12
+export const FEATURES = {
13
+    INBOUND_CALL: 'inbound-call',
14
+    OUTBOUND_CALL: 'outbound-call',
15
+    RECORDING: 'recording',
16
+    SIP_INBOUND_CALL: 'sip-inbound-call',
17
+    SIP_OUTBOUND_CALL: 'sip-outbound-call',
18
+    STREAMING: 'streaming',
19
+    TRANSCRIPTION: 'transcription'
20
+};
21
+
22
+/**
23
+ * URL for displaying JaaS upgrade options
24
+ */
25
+export const JAAS_UPGRADE_URL = 'https://jaas.8x8.vc/#/plan/upgrade';

+ 48
- 0
react/features/jaas/functions.js View File

@@ -0,0 +1,48 @@
1
+// @flow
2
+
3
+/**
4
+ * Sends a request for retrieving jaas customer details.
5
+ *
6
+ * @param {Object} reqData - The request info.
7
+ * @param {string} reqData.appId - The client appId.
8
+ * @param {string} reqData.baseUrl - The base url for the request.
9
+ * @param {string} reqData.jwt - The JWT token.
10
+ * @returns {void}
11
+ */
12
+export async function sendGetDetailsRequest({ appId, baseUrl, jwt }: {
13
+    appId: string,
14
+    baseUrl: string,
15
+    jwt: string,
16
+}) {
17
+    const fullUrl = `${baseUrl}/v1/customers/${encodeURIComponent(appId)}`;
18
+    const headers = {
19
+        'Authorization': `Bearer ${jwt}`
20
+    };
21
+
22
+    try {
23
+        const res = await fetch(fullUrl, {
24
+            method: 'GET',
25
+            headers
26
+        });
27
+
28
+        if (res.ok) {
29
+            return res.json();
30
+        }
31
+
32
+        throw new Error('Request not successful');
33
+    } catch (err) {
34
+        throw new Error(err);
35
+
36
+    }
37
+}
38
+
39
+/**
40
+ * Returns the billing id for vpaas meetings.
41
+ *
42
+ * @param {Object} state - The state of the app.
43
+ * @param {string} feature - Feature to be looked up for disable state.
44
+ * @returns {boolean}
45
+ */
46
+export function isFeatureDisabled(state: Object, feature: string) {
47
+    return state['features/jaas'].disabledFeatures.includes(feature);
48
+}

+ 5
- 0
react/features/jaas/logger.js View File

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

+ 33
- 0
react/features/jaas/middleware.web.js View File

@@ -0,0 +1,33 @@
1
+import { redirectToStaticPage } from '../app/actions';
2
+import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
3
+import { MiddlewareRegistry } from '../base/redux';
4
+
5
+import { SET_DETAILS } from './actionTypes';
6
+import { getCustomerDetails } from './actions';
7
+import { STATUSES } from './constants';
8
+
9
+/**
10
+ * The redux middleware for billing counter.
11
+ *
12
+ * @param {Store} store - The redux store.
13
+ * @returns {Function}
14
+ */
15
+
16
+MiddlewareRegistry.register(store => next => async action => {
17
+    switch (action.type) {
18
+    case CONFERENCE_JOINED: {
19
+        store.dispatch(getCustomerDetails());
20
+        break;
21
+    }
22
+
23
+    case SET_DETAILS: {
24
+        const { status } = action.payload;
25
+
26
+        if (status === STATUSES.BLOCKED) {
27
+            store.dispatch(redirectToStaticPage('/static/planLimit.html'));
28
+        }
29
+    }
30
+    }
31
+
32
+    return next(action);
33
+});

+ 28
- 0
react/features/jaas/reducer.js View File

@@ -0,0 +1,28 @@
1
+import { ReducerRegistry } from '../base/redux';
2
+
3
+import {
4
+    SET_DETAILS
5
+} from './actionTypes';
6
+import { STATUSES } from './constants';
7
+
8
+const DEFAULT_STATE = {
9
+    disabledFeatures: [],
10
+    status: STATUSES.ACTIVE
11
+};
12
+
13
+/**
14
+ * Listen for actions that mutate the billing-counter state
15
+ */
16
+ReducerRegistry.register(
17
+    'features/jaas', (state = DEFAULT_STATE, action) => {
18
+        switch (action.type) {
19
+
20
+        case SET_DETAILS: {
21
+            return action.payload;
22
+        }
23
+
24
+        default:
25
+            return state;
26
+        }
27
+    },
28
+);

+ 10
- 4
react/features/recording/components/LiveStream/AbstractLiveStreamButton.js View File

@@ -8,6 +8,8 @@ import {
8 8
     isLocalParticipantModerator
9 9
 } from '../../../base/participants';
10 10
 import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
11
+import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
12
+import { FEATURES } from '../../../jaas/constants';
11 13
 import { getActiveSession } from '../../functions';
12 14
 
13 15
 import {
@@ -73,12 +75,16 @@ export default class AbstractLiveStreamButton<P: Props> extends AbstractButton<P
73 75
      * @protected
74 76
      * @returns {void}
75 77
      */
76
-    _handleClick() {
78
+    async _handleClick() {
77 79
         const { _isLiveStreamRunning, dispatch } = this.props;
78 80
 
79
-        dispatch(openDialog(
80
-            _isLiveStreamRunning ? StopLiveStreamDialog : StartLiveStreamDialog
81
-        ));
81
+        const dialogShown = await dispatch(maybeShowPremiumFeatureDialog(FEATURES.RECORDING));
82
+
83
+        if (!dialogShown) {
84
+            dispatch(openDialog(
85
+                _isLiveStreamRunning ? StopLiveStreamDialog : StartLiveStreamDialog
86
+            ));
87
+        }
82 88
     }
83 89
 
84 90
     /**

+ 10
- 4
react/features/recording/components/Recording/AbstractRecordButton.js View File

@@ -12,6 +12,8 @@ import {
12 12
     isLocalParticipantModerator
13 13
 } from '../../../base/participants';
14 14
 import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
15
+import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
16
+import { FEATURES } from '../../../jaas/constants';
15 17
 import { getActiveSession } from '../../functions';
16 18
 
17 19
 import { StartRecordingDialog, StopRecordingDialog } from './_';
@@ -74,7 +76,7 @@ export default class AbstractRecordButton<P: Props> extends AbstractButton<P, *>
74 76
      * @protected
75 77
      * @returns {void}
76 78
      */
77
-    _handleClick() {
79
+    async _handleClick() {
78 80
         const { _isRecordingRunning, dispatch } = this.props;
79 81
 
80 82
         sendAnalytics(createToolbarEvent(
@@ -84,9 +86,13 @@ export default class AbstractRecordButton<P: Props> extends AbstractButton<P, *>
84 86
                 type: JitsiRecordingConstants.mode.FILE
85 87
             }));
86 88
 
87
-        dispatch(openDialog(
88
-            _isRecordingRunning ? StopRecordingDialog : StartRecordingDialog
89
-        ));
89
+        const dialogShown = await dispatch(maybeShowPremiumFeatureDialog(FEATURES.RECORDING));
90
+
91
+        if (!dialogShown) {
92
+            dispatch(openDialog(
93
+                _isRecordingRunning ? StopRecordingDialog : StartRecordingDialog
94
+            ));
95
+        }
90 96
     }
91 97
 
92 98
     /**

+ 9
- 2
react/features/subtitles/components/AbstractClosedCaptionButton.js View File

@@ -3,6 +3,8 @@
3 3
 import { createToolbarEvent, sendAnalytics } from '../../analytics';
4 4
 import { isLocalParticipantModerator } from '../../base/participants';
5 5
 import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
6
+import { maybeShowPremiumFeatureDialog } from '../../jaas/actions';
7
+import { FEATURES } from '../../jaas/constants';
6 8
 import { toggleRequestingSubtitles } from '../actions';
7 9
 
8 10
 export type AbstractProps = AbstractButtonProps & {
@@ -35,7 +37,7 @@ export class AbstractClosedCaptionButton
35 37
      * @protected
36 38
      * @returns {void}
37 39
      */
38
-    _handleClick() {
40
+    async _handleClick() {
39 41
         const { _requestingSubtitles, dispatch } = this.props;
40 42
 
41 43
         sendAnalytics(createToolbarEvent('transcribing.ccButton',
@@ -43,7 +45,12 @@ export class AbstractClosedCaptionButton
43 45
                 'requesting_subtitles': Boolean(_requestingSubtitles)
44 46
             }));
45 47
 
46
-        dispatch(toggleRequestingSubtitles());
48
+
49
+        const dialogShown = await dispatch(maybeShowPremiumFeatureDialog(FEATURES.RECORDING));
50
+
51
+        if (!dialogShown) {
52
+            dispatch(toggleRequestingSubtitles());
53
+        }
47 54
     }
48 55
 
49 56
     /**

+ 0
- 0
static/planLimit.html View File


Loading…
Cancel
Save