feat(captain): Add paywall and expose Custom Tools (#13977)
# Pull Request Template ## Description Custom tools is now discoverable on all plans ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Before: <img width="390" height="446" alt="CleanShot 2026-04-02 at 13 40 11@2x" src="https://github.com/user-attachments/assets/0a751954-f3ad-47d6-85b8-1e2f1476a646" /> After: <img width="392" height="522" alt="CleanShot 2026-04-02 at 13 40 47@2x" src="https://github.com/user-attachments/assets/62a252f6-2551-47a9-b50c-be949f08c456" /> <img width="1826" height="638" alt="CleanShot 2026-04-02 at 13 37 39@2x" src="https://github.com/user-attachments/assets/77dc2a75-3d76-44cf-8579-8d3457879bd0" /> ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
@@ -6,6 +6,13 @@ import { useAccount } from 'dashboard/composables/useAccount';
|
|||||||
|
|
||||||
import BasePaywallModal from 'dashboard/routes/dashboard/settings/components/BasePaywallModal.vue';
|
import BasePaywallModal from 'dashboard/routes/dashboard/settings/components/BasePaywallModal.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
featurePrefix: {
|
||||||
|
type: String,
|
||||||
|
default: 'CAPTAIN',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const currentUser = useMapGetter('getCurrentUser');
|
const currentUser = useMapGetter('getCurrentUser');
|
||||||
|
|
||||||
@@ -31,7 +38,7 @@ const openBilling = () => {
|
|||||||
>
|
>
|
||||||
<BasePaywallModal
|
<BasePaywallModal
|
||||||
class="mx-auto"
|
class="mx-auto"
|
||||||
feature-prefix="CAPTAIN"
|
:feature-prefix="featurePrefix"
|
||||||
:i18n-key="i18nKey"
|
:i18n-key="i18nKey"
|
||||||
:is-super-admin="isSuperAdmin"
|
:is-super-admin="isSuperAdmin"
|
||||||
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
||||||
|
|||||||
@@ -63,16 +63,6 @@ const hasAdvancedAssignment = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasCustomTools = computed(() => {
|
|
||||||
return (
|
|
||||||
isFeatureEnabledonAccount.value(
|
|
||||||
accountId.value,
|
|
||||||
FEATURE_FLAGS.CAPTAIN_CUSTOM_TOOLS
|
|
||||||
) ||
|
|
||||||
isFeatureEnabledonAccount.value(accountId.value, FEATURE_FLAGS.CAPTAIN_V2)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleShortcutModalFn = show => {
|
const toggleShortcutModalFn = show => {
|
||||||
if (show) {
|
if (show) {
|
||||||
emit('openKeyShortcutModal');
|
emit('openKeyShortcutModal');
|
||||||
@@ -374,18 +364,14 @@ const menuItems = computed(() => {
|
|||||||
navigationPath: 'captain_assistants_inboxes_index',
|
navigationPath: 'captain_assistants_inboxes_index',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
...(hasCustomTools.value
|
{
|
||||||
? [
|
name: 'Tools',
|
||||||
{
|
label: t('SIDEBAR.CAPTAIN_TOOLS'),
|
||||||
name: 'Tools',
|
activeOn: ['captain_tools_index'],
|
||||||
label: t('SIDEBAR.CAPTAIN_TOOLS'),
|
to: accountScopedRoute('captain_assistants_index', {
|
||||||
activeOn: ['captain_tools_index'],
|
navigationPath: 'captain_tools_index',
|
||||||
to: accountScopedRoute('captain_assistants_index', {
|
}),
|
||||||
navigationPath: 'captain_tools_index',
|
},
|
||||||
}),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
label: t('SIDEBAR.CAPTAIN_SETTINGS'),
|
label: t('SIDEBAR.CAPTAIN_SETTINGS'),
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const FEATURE_FLAGS = {
|
|||||||
export const PREMIUM_FEATURES = [
|
export const PREMIUM_FEATURES = [
|
||||||
FEATURE_FLAGS.SLA,
|
FEATURE_FLAGS.SLA,
|
||||||
FEATURE_FLAGS.CAPTAIN,
|
FEATURE_FLAGS.CAPTAIN,
|
||||||
|
FEATURE_FLAGS.CAPTAIN_CUSTOM_TOOLS,
|
||||||
FEATURE_FLAGS.CUSTOM_ROLES,
|
FEATURE_FLAGS.CUSTOM_ROLES,
|
||||||
FEATURE_FLAGS.AUDIT_LOGS,
|
FEATURE_FLAGS.AUDIT_LOGS,
|
||||||
FEATURE_FLAGS.HELP_CENTER,
|
FEATURE_FLAGS.HELP_CENTER,
|
||||||
|
|||||||
@@ -838,6 +838,18 @@
|
|||||||
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
|
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
|
||||||
"ERROR_MESSAGE": "Failed to delete custom tool"
|
"ERROR_MESSAGE": "Failed to delete custom tool"
|
||||||
},
|
},
|
||||||
|
"PAYWALL": {
|
||||||
|
"TITLE": "Upgrade to use tools with Captain",
|
||||||
|
"AVAILABLE_ON": "Captain Tools are only available in Business and Enterprise plans. Please upgrade to Business plan to use the feature.",
|
||||||
|
"UPGRADE_PROMPT": "",
|
||||||
|
"UPGRADE_NOW": "Open billing",
|
||||||
|
"CANCEL_ANYTIME": ""
|
||||||
|
},
|
||||||
|
"ENTERPRISE_PAYWALL": {
|
||||||
|
"AVAILABLE_ON": "Captain Tools are only available in the paid plans.",
|
||||||
|
"UPGRADE_PROMPT": "Please upgrade to a paid plan to use this feature.",
|
||||||
|
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
||||||
|
},
|
||||||
"TEST": {
|
"TEST": {
|
||||||
"BUTTON": "Test connection",
|
"BUTTON": "Test connection",
|
||||||
"SUCCESS": "Endpoint returned HTTP {status}",
|
"SUCCESS": "Endpoint returned HTTP {status}",
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ const meta = {
|
|||||||
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
|
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const metaCustomTools = {
|
||||||
|
permissions: ['administrator', 'agent'],
|
||||||
|
featureFlag: FEATURE_FLAGS.CAPTAIN_CUSTOM_TOOLS,
|
||||||
|
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
|
||||||
|
};
|
||||||
|
|
||||||
const metaV2 = {
|
const metaV2 = {
|
||||||
permissions: ['administrator', 'agent'],
|
permissions: ['administrator', 'agent'],
|
||||||
featureFlag: FEATURE_FLAGS.CAPTAIN_V2,
|
featureFlag: FEATURE_FLAGS.CAPTAIN_V2,
|
||||||
@@ -46,7 +52,7 @@ const assistantRoutes = [
|
|||||||
path: frontendURL('accounts/:accountId/captain/:assistantId/tools'),
|
path: frontendURL('accounts/:accountId/captain/:assistantId/tools'),
|
||||||
component: CustomToolsIndex,
|
component: CustomToolsIndex,
|
||||||
name: 'captain_tools_index',
|
name: 'captain_tools_index',
|
||||||
meta,
|
meta: metaCustomTools,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: frontendURL('accounts/:accountId/captain/:assistantId/scenarios'),
|
path: frontendURL('accounts/:accountId/captain/:assistantId/scenarios'),
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
|||||||
import { usePolicy } from 'dashboard/composables/usePolicy';
|
import { usePolicy } from 'dashboard/composables/usePolicy';
|
||||||
|
|
||||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||||
|
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
|
||||||
import CustomToolsPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/CustomToolsPageEmptyState.vue';
|
import CustomToolsPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/CustomToolsPageEmptyState.vue';
|
||||||
import CreateCustomToolDialog from 'dashboard/components-next/captain/pageComponents/customTool/CreateCustomToolDialog.vue';
|
import CreateCustomToolDialog from 'dashboard/components-next/captain/pageComponents/customTool/CreateCustomToolDialog.vue';
|
||||||
import CustomToolCard from 'dashboard/components-next/captain/pageComponents/customTool/CustomToolCard.vue';
|
import CustomToolCard from 'dashboard/components-next/captain/pageComponents/customTool/CustomToolCard.vue';
|
||||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { isFeatureFlagEnabled } = usePolicy();
|
const { isFeatureFlagEnabled, shouldShowPaywall } = usePolicy();
|
||||||
|
|
||||||
const SOFT_LIMIT = 10;
|
const SOFT_LIMIT = 10;
|
||||||
const isV2 = computed(() => isFeatureFlagEnabled(FEATURE_FLAGS.CAPTAIN_V2));
|
const isV2 = computed(() => isFeatureFlagEnabled(FEATURE_FLAGS.CAPTAIN_V2));
|
||||||
@@ -80,7 +81,9 @@ const onDeleteSuccess = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchCustomTools();
|
if (!shouldShowPaywall(FEATURE_FLAGS.CAPTAIN_CUSTOM_TOOLS)) {
|
||||||
|
fetchCustomTools();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -89,6 +92,7 @@ onMounted(() => {
|
|||||||
:header-title="$t('CAPTAIN.CUSTOM_TOOLS.HEADER')"
|
:header-title="$t('CAPTAIN.CUSTOM_TOOLS.HEADER')"
|
||||||
:button-label="$t('CAPTAIN.CUSTOM_TOOLS.ADD_NEW')"
|
:button-label="$t('CAPTAIN.CUSTOM_TOOLS.ADD_NEW')"
|
||||||
:button-policy="['administrator']"
|
:button-policy="['administrator']"
|
||||||
|
:feature-flag="FEATURE_FLAGS.CAPTAIN_CUSTOM_TOOLS"
|
||||||
:total-count="customToolsMeta.totalCount"
|
:total-count="customToolsMeta.totalCount"
|
||||||
:current-page="customToolsMeta.page"
|
:current-page="customToolsMeta.page"
|
||||||
:show-pagination-footer="!isFetching && !!customTools.length"
|
:show-pagination-footer="!isFetching && !!customTools.length"
|
||||||
@@ -98,6 +102,10 @@ onMounted(() => {
|
|||||||
@update:current-page="onPageChange"
|
@update:current-page="onPageChange"
|
||||||
@click="openCreateDialog"
|
@click="openCreateDialog"
|
||||||
>
|
>
|
||||||
|
<template #paywall>
|
||||||
|
<CaptainPaywall feature-prefix="CAPTAIN.CUSTOM_TOOLS" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #emptyState>
|
<template #emptyState>
|
||||||
<CustomToolsPageEmptyState @click="openCreateDialog" />
|
<CustomToolsPageEmptyState @click="openCreateDialog" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||||
import Icon from 'next/icon/Icon.vue';
|
import Icon from 'next/icon/Icon.vue';
|
||||||
import ButtonV4 from 'next/button/Button.vue';
|
import ButtonV4 from 'next/button/Button.vue';
|
||||||
|
|
||||||
@@ -22,6 +23,11 @@ defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['upgrade']);
|
const emit = defineEmits(['upgrade']);
|
||||||
|
|
||||||
|
// Cloud agents land on this modal too, but billing is admin-only — they need
|
||||||
|
// the escalation message instead of a button they cannot use. Mirrors the
|
||||||
|
// pattern in UpgradePage.vue.
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -47,11 +53,14 @@ const emit = defineEmits(['upgrade']);
|
|||||||
/>
|
/>
|
||||||
<p class="text-sm font-normal text-n-slate-11">
|
<p class="text-sm font-normal text-n-slate-11">
|
||||||
{{ $t(`${featurePrefix}.${i18nKey}.UPGRADE_PROMPT`) }}
|
{{ $t(`${featurePrefix}.${i18nKey}.UPGRADE_PROMPT`) }}
|
||||||
<span v-if="!isOnChatwootCloud && !isSuperAdmin">
|
<span v-if="isOnChatwootCloud && !isAdmin">
|
||||||
|
{{ $t('GENERAL_SETTINGS.LIMIT_MESSAGES.NON_ADMIN') }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="!isOnChatwootCloud && !isSuperAdmin">
|
||||||
{{ $t(`${featurePrefix}.ENTERPRISE_PAYWALL.ASK_ADMIN`) }}
|
{{ $t(`${featurePrefix}.ENTERPRISE_PAYWALL.ASK_ADMIN`) }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<template v-if="isOnChatwootCloud">
|
<template v-if="isOnChatwootCloud && isAdmin">
|
||||||
<ButtonV4 blue solid md @click="emit('upgrade')">
|
<ButtonV4 blue solid md @click="emit('upgrade')">
|
||||||
{{ $t(`${featurePrefix}.PAYWALL.UPGRADE_NOW`) }}
|
{{ $t(`${featurePrefix}.PAYWALL.UPGRADE_NOW`) }}
|
||||||
</ButtonV4>
|
</ButtonV4>
|
||||||
@@ -59,7 +68,7 @@ const emit = defineEmits(['upgrade']);
|
|||||||
{{ $t(`${featurePrefix}.PAYWALL.CANCEL_ANYTIME`) }}
|
{{ $t(`${featurePrefix}.PAYWALL.CANCEL_ANYTIME`) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="isSuperAdmin">
|
<template v-else-if="!isOnChatwootCloud && isSuperAdmin">
|
||||||
<a href="/super_admin" class="block w-full">
|
<a href="/super_admin" class="block w-full">
|
||||||
<ButtonV4 solid blue md class="w-full">
|
<ButtonV4 solid blue md class="w-full">
|
||||||
{{ $t(`${featurePrefix}.PAYWALL.UPGRADE_NOW`) }}
|
{{ $t(`${featurePrefix}.PAYWALL.UPGRADE_NOW`) }}
|
||||||
|
|||||||
Reference in New Issue
Block a user