diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index f63e8f6bf..0f915ab8a 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -36,7 +36,7 @@ class DashboardController < ActionController::Base 'LOGOUT_REDIRECT_LINK', 'DISABLE_USER_PROFILE_UPDATE', 'DEPLOYMENT_ENV', - 'CSML_EDITOR_HOST' + 'CSML_EDITOR_HOST', 'INSTALLATION_PRICING_PLAN' ).merge(app_config) end diff --git a/app/javascript/dashboard/components-next/captain/PageLayout.vue b/app/javascript/dashboard/components-next/captain/PageLayout.vue index 1ce03cec5..dd76f1b62 100644 --- a/app/javascript/dashboard/components-next/captain/PageLayout.vue +++ b/app/javascript/dashboard/components-next/captain/PageLayout.vue @@ -1,12 +1,12 @@ diff --git a/app/javascript/dashboard/composables/usePolicy.js b/app/javascript/dashboard/composables/usePolicy.js index 0653003ec..dc203cbac 100644 --- a/app/javascript/dashboard/composables/usePolicy.js +++ b/app/javascript/dashboard/composables/usePolicy.js @@ -1,20 +1,31 @@ +import { computed, unref } from 'vue'; import { useMapGetter } from 'dashboard/composables/store'; import { useAccount } from 'dashboard/composables/useAccount'; +import { useConfig } from 'dashboard/composables/useConfig'; import { getUserPermissions, hasPermissions, } from 'dashboard/helper/permissionsHelper'; +import { PREMIUM_FEATURES } from 'dashboard/featureFlags'; + +import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes'; export function usePolicy() { const user = useMapGetter('getCurrentUser'); const isFeatureEnabled = useMapGetter('accounts/isFeatureEnabledonAccount'); + const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud'); + const isACustomBrandedInstance = useMapGetter( + 'globalConfig/isACustomBrandedInstance' + ); + + const { isEnterprise, enterprisePlanName } = useConfig(); const { accountId } = useAccount(); const getUserPermissionsForAccount = () => { return getUserPermissions(user.value, accountId.value); }; - const checkFeatureAllowed = featureFlag => { + const isFeatureFlagEnabled = featureFlag => { if (!featureFlag) return true; return isFeatureEnabled.value(accountId.value, featureFlag); }; @@ -25,5 +36,99 @@ export function usePolicy() { return hasPermissions(requiredPermissions, userPermissions); }; - return { checkFeatureAllowed, checkPermissions }; + const checkInstallationType = config => { + if (Array.isArray(config) && config.length > 0) { + const installationCheck = { + [INSTALLATION_TYPES.ENTERPRISE]: isEnterprise, + [INSTALLATION_TYPES.CLOUD]: isOnChatwootCloud.value, + [INSTALLATION_TYPES.COMMUNITY]: true, + }; + + return config.some(type => installationCheck[type]); + } + + return true; + }; + + const isPremiumFeature = featureFlag => { + if (!featureFlag) return true; + return PREMIUM_FEATURES.includes(featureFlag); + }; + + const hasPremiumEnterprise = computed(() => { + if (isEnterprise) return enterprisePlanName !== 'community'; + + return true; + }); + + const shouldShow = (featureFlag, permissions, installationTypes) => { + const flag = unref(featureFlag); + const perms = unref(permissions); + const installation = unref(installationTypes); + + // if the user does not have permissions or installation type is not supported + // return false; + // This supersedes everything + if (!checkPermissions(perms)) return false; + if (!checkInstallationType(installation)) return false; + + if (isACustomBrandedInstance.value) { + // if this is a custom branded instance, we just use the feature flag as a reference + return isFeatureFlagEnabled(flag); + } + + // if on cloud, we should if the feature is allowed + // or if the feature is a premium one like SLA to show a paywall + // the paywall should be managed by the individual component + if (isOnChatwootCloud.value) { + return isFeatureFlagEnabled(flag) || isPremiumFeature(flag); + } + + if (isEnterprise) { + // in enterprise, if the feature is premium but they don't have an enterprise plan + // we should it anyway this is to show upsells on enterprise regardless of the feature flag + // Feature flag is only honored if they have a premium plan + // + // In case they have a premium plan, the check on feature flag alone is enough + // because the second condition will always be false + // That means once subscribed, the feature can be disabled by the admin + // + // the paywall should be managed by the individual component + return ( + isFeatureFlagEnabled(flag) || + (isPremiumFeature(flag) && !hasPremiumEnterprise.value) + ); + } + + // default to true + return true; + }; + + const shouldShowPaywall = featureFlag => { + const flag = unref(featureFlag); + if (!flag) return false; + + if (isACustomBrandedInstance.value) { + // custom branded instances never show paywall + return false; + } + + if (isPremiumFeature(flag)) { + if (isOnChatwootCloud.value) { + return !isFeatureFlagEnabled(flag); + } + + if (isEnterprise) { + return !hasPremiumEnterprise.value; + } + } + + return false; + }; + + return { + checkPermissions, + shouldShowPaywall, + shouldShow, + }; } diff --git a/app/javascript/dashboard/constants/installationTypes.js b/app/javascript/dashboard/constants/installationTypes.js new file mode 100644 index 000000000..87709e98d --- /dev/null +++ b/app/javascript/dashboard/constants/installationTypes.js @@ -0,0 +1,5 @@ +export const INSTALLATION_TYPES = { + CLOUD: 'cloud', + ENTERPRISE: 'enterprise', + COMMUNITY: 'community', +}; diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js index 0695be473..f7430bdba 100644 --- a/app/javascript/dashboard/featureFlags.js +++ b/app/javascript/dashboard/featureFlags.js @@ -36,3 +36,11 @@ export const FEATURE_FLAGS = { REPORT_V4: 'report_v4', CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team', }; + +export const PREMIUM_FEATURES = [ + FEATURE_FLAGS.SLA, + FEATURE_FLAGS.CAPTAIN, + FEATURE_FLAGS.CUSTOM_ROLES, + FEATURE_FLAGS.AUDIT_LOGS, + FEATURE_FLAGS.HELP_CENTER, +]; diff --git a/app/javascript/dashboard/routes/dashboard/campaigns/campaigns.routes.js b/app/javascript/dashboard/routes/dashboard/campaigns/campaigns.routes.js index a4f0a94f5..e0c1f3a17 100644 --- a/app/javascript/dashboard/routes/dashboard/campaigns/campaigns.routes.js +++ b/app/javascript/dashboard/routes/dashboard/campaigns/campaigns.routes.js @@ -3,6 +3,12 @@ import { frontendURL } from 'dashboard/helper/URLHelper.js'; import CampaignsPageRouteView from './pages/CampaignsPageRouteView.vue'; import LiveChatCampaignsPage from './pages/LiveChatCampaignsPage.vue'; import SMSCampaignsPage from './pages/SMSCampaignsPage.vue'; +import { FEATURE_FLAGS } from 'dashboard/featureFlags'; + +const meta = { + featureFlag: FEATURE_FLAGS.CAMPAIGNS, + permissions: ['administrator'], +}; const campaignsRoutes = { routes: [ @@ -19,9 +25,7 @@ const campaignsRoutes = { { path: 'ongoing', name: 'campaigns_ongoing_index', - meta: { - permissions: ['administrator'], - }, + meta, redirect: to => { return { name: 'campaigns_livechat_index', params: to.params }; }, @@ -29,9 +33,7 @@ const campaignsRoutes = { { path: 'one_off', name: 'campaigns_one_off_index', - meta: { - permissions: ['administrator'], - }, + meta, redirect: to => { return { name: 'campaigns_sms_index', params: to.params }; }, @@ -39,17 +41,13 @@ const campaignsRoutes = { { path: 'live_chat', name: 'campaigns_livechat_index', - meta: { - permissions: ['administrator'], - }, + meta, component: LiveChatCampaignsPage, }, { path: 'sms', name: 'campaigns_sms_index', - meta: { - permissions: ['administrator'], - }, + meta, component: SMSCampaignsPage, }, ], diff --git a/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue b/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue index 3ea78e74a..bcc551966 100644 --- a/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue @@ -78,8 +78,8 @@ onMounted(() => store.dispatch('captainAssistants/get')); :button-policy="['administrator']" :show-pagination-footer="false" :is-fetching="isFetching" - :feature-flag="FEATURE_FLAGS.CAPTAIN" :is-empty="!assistants.length" + :feature-flag="FEATURE_FLAGS.CAPTAIN" @click="handleCreate" >