mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
feat: Upgrade page instead of banner (#11202)
# Pull Request Template ## Description This PR will replace the upgrade banner with an upgrade page view. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Loom video https://www.loom.com/share/0f2b4b09acdd4404bf3211184a470227?sid=7ed60a99-0299-4642-b907-2af8c4dcc643 ## 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 - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] 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 - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
@@ -18,4 +18,8 @@ module BillingHelper
|
||||
def non_web_inboxes(account)
|
||||
account.inboxes.where.not(channel_type: Channel::WebWidget.to_s).count
|
||||
end
|
||||
|
||||
def agents(account)
|
||||
account.users.count
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,6 @@ import AddAccountModal from '../dashboard/components/layout/sidebarComponents/Ad
|
||||
import LoadingState from './components/widgets/LoadingState.vue';
|
||||
import NetworkNotification from './components/NetworkNotification.vue';
|
||||
import UpdateBanner from './components/app/UpdateBanner.vue';
|
||||
import UpgradeBanner from './components/app/UpgradeBanner.vue';
|
||||
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
|
||||
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
|
||||
import vueActionCable from './helper/actionCable';
|
||||
@@ -31,7 +30,6 @@ export default {
|
||||
UpdateBanner,
|
||||
PaymentPendingBanner,
|
||||
WootSnackbarBox,
|
||||
UpgradeBanner,
|
||||
PendingEmailVerificationBanner,
|
||||
},
|
||||
setup() {
|
||||
@@ -146,7 +144,6 @@ export default {
|
||||
<template v-if="currentAccountId">
|
||||
<PendingEmailVerificationBanner v-if="hideOnOnboardingView" />
|
||||
<PaymentPendingBanner v-if="hideOnOnboardingView" />
|
||||
<UpgradeBanner />
|
||||
</template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"GENERAL_SETTINGS": {
|
||||
"LIMIT_MESSAGES": {
|
||||
"CONVERSATION": "You have exceeded the conversation limit. Hacker plan allows only 500 conversations.",
|
||||
"INBOXES": "You have exceeded the inbox limit. Hacker plan only supports website live-chat. Additional inboxes like email, WhatsApp etc. require a paid plan.",
|
||||
"AGENTS": "You have exceeded the agent limit. Hacker plan allows only 2 agents.",
|
||||
"NON_ADMIN": "Please contact your administrator to upgrade the plan and continue using all features."
|
||||
},
|
||||
"TITLE": "Account settings",
|
||||
"SUBMIT": "Update settings",
|
||||
"BACK": "Back",
|
||||
@@ -51,6 +57,7 @@
|
||||
"UPDATE_CHATWOOT": "An update {latestChatwootVersion} for Chatwoot is available. Please update your instance.",
|
||||
"LEARN_MORE": "Learn more",
|
||||
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
|
||||
"UPGRADE": "Upgrade to continue using Chatwoot",
|
||||
"LIMITS_UPGRADE": "Your account has exceeded the usage limits, please upgrade your plan to continue using Chatwoot",
|
||||
"OPEN_BILLING": "Open billing"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
import NextSidebar from 'next/sidebar/Sidebar.vue';
|
||||
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
|
||||
@@ -8,6 +8,7 @@ import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAc
|
||||
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
|
||||
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
|
||||
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
||||
import UpgradePage from 'dashboard/routes/dashboard/upgrade/UpgradePage.vue';
|
||||
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
@@ -35,8 +36,10 @@ export default {
|
||||
AccountSelector,
|
||||
AddLabelModal,
|
||||
NotificationPanel,
|
||||
UpgradePage,
|
||||
},
|
||||
setup() {
|
||||
const upgradePageRef = ref(null);
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const { accountId } = useAccount();
|
||||
|
||||
@@ -44,6 +47,7 @@ export default {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
accountId,
|
||||
upgradePageRef,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -64,6 +68,16 @@ export default {
|
||||
currentRoute() {
|
||||
return ' ';
|
||||
},
|
||||
showUpgradePage() {
|
||||
return this.upgradePageRef?.shouldShowUpgradePage;
|
||||
},
|
||||
bypassUpgradePage() {
|
||||
return [
|
||||
'billing_settings_index',
|
||||
'settings_inbox_list',
|
||||
'agent_list',
|
||||
].includes(this.$route.name);
|
||||
},
|
||||
isSidebarOpen() {
|
||||
const { show_secondary_sidebar: showSecondarySidebar } = this.uiSettings;
|
||||
return showSecondarySidebar;
|
||||
@@ -197,8 +211,25 @@ export default {
|
||||
@show-add-label-popup="showAddLabelPopup"
|
||||
/>
|
||||
<main class="flex flex-1 h-full min-h-0 px-0 overflow-hidden">
|
||||
<router-view />
|
||||
<CommandBar />
|
||||
<UpgradePage
|
||||
v-show="showUpgradePage"
|
||||
ref="upgradePageRef"
|
||||
:bypass-upgrade-page="bypassUpgradePage"
|
||||
/>
|
||||
<template v-if="!showUpgradePage">
|
||||
<router-view />
|
||||
<CommandBar />
|
||||
<NotificationPanel
|
||||
v-if="isNotificationPanel"
|
||||
@close="closeNotificationPanel"
|
||||
/>
|
||||
<woot-modal
|
||||
v-model:show="showAddLabelModal"
|
||||
:on-close="hideAddLabelPopup"
|
||||
>
|
||||
<AddLabelModal @close="hideAddLabelPopup" />
|
||||
</woot-modal>
|
||||
</template>
|
||||
<AccountSelector
|
||||
:show-account-modal="showAccountModal"
|
||||
@close-account-modal="toggleAccountModal"
|
||||
@@ -213,16 +244,6 @@ export default {
|
||||
@close="closeKeyShortcutModal"
|
||||
@clickaway="closeKeyShortcutModal"
|
||||
/>
|
||||
<NotificationPanel
|
||||
v-if="isNotificationPanel"
|
||||
@close="closeNotificationPanel"
|
||||
/>
|
||||
<woot-modal
|
||||
v-model:show="showAddLabelModal"
|
||||
:on-close="hideAddLabelPopup"
|
||||
>
|
||||
<AddLabelModal @close="hideAddLabelPopup" />
|
||||
</woot-modal>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
<script setup>
|
||||
import { onMounted, computed, defineExpose, defineProps } from 'vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
bypassUpgradePage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const { accountId, currentAccount } = useAccount();
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
|
||||
|
||||
const testLimit = ({ allowed, consumed }) => {
|
||||
return consumed > allowed;
|
||||
};
|
||||
|
||||
const isTrialAccount = computed(() => {
|
||||
// check if account is less than 15 days old
|
||||
const account = currentAccount.value;
|
||||
if (!account) return false;
|
||||
|
||||
const createdAt = new Date(account.created_at);
|
||||
const diffDays = differenceInDays(new Date(), createdAt);
|
||||
|
||||
return diffDays <= 15;
|
||||
});
|
||||
|
||||
const limitExceededMessage = computed(() => {
|
||||
const account = currentAccount.value;
|
||||
if (!account?.limits) return '';
|
||||
|
||||
const {
|
||||
conversation,
|
||||
non_web_inboxes: nonWebInboxes,
|
||||
agents,
|
||||
} = account.limits;
|
||||
|
||||
let message = '';
|
||||
|
||||
if (testLimit(conversation)) {
|
||||
message = t('GENERAL_SETTINGS.LIMIT_MESSAGES.CONVERSATION');
|
||||
} else if (testLimit(nonWebInboxes)) {
|
||||
message = t('GENERAL_SETTINGS.LIMIT_MESSAGES.INBOXES');
|
||||
} else if (testLimit(agents)) {
|
||||
message = t('GENERAL_SETTINGS.LIMIT_MESSAGES.AGENTS');
|
||||
}
|
||||
|
||||
return message;
|
||||
});
|
||||
|
||||
const isLimitExceeded = computed(() => {
|
||||
const account = currentAccount.value;
|
||||
if (!account?.limits) return false;
|
||||
|
||||
const {
|
||||
conversation,
|
||||
non_web_inboxes: nonWebInboxes,
|
||||
agents,
|
||||
} = account.limits;
|
||||
|
||||
return (
|
||||
testLimit(conversation) || testLimit(nonWebInboxes) || testLimit(agents)
|
||||
);
|
||||
});
|
||||
|
||||
const shouldShowUpgradePage = computed(() => {
|
||||
// Skip upgrade page in Billing, Inbox, and Agent pages
|
||||
if (props.bypassUpgradePage) return false;
|
||||
if (!isOnChatwootCloud.value) return false;
|
||||
if (isTrialAccount.value) return false;
|
||||
return isLimitExceeded.value;
|
||||
});
|
||||
|
||||
const fetchLimits = () => {
|
||||
store.dispatch('accounts/limits');
|
||||
};
|
||||
|
||||
const routeToBilling = () => {
|
||||
router.push({
|
||||
name: 'billing_settings_index',
|
||||
params: { accountId: accountId.value },
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => fetchLimits());
|
||||
|
||||
defineExpose({ shouldShowUpgradePage });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="shouldShowUpgradePage">
|
||||
<div class="mx-auto h-full pt-[clamp(3rem,15vh,12rem)]">
|
||||
<div
|
||||
class="flex flex-col gap-4 max-w-md px-8 py-6 shadow-lg bg-n-solid-1 rounded-xl outline outline-1 outline-n-container"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center w-full gap-2">
|
||||
<span
|
||||
class="flex items-center justify-center w-6 h-6 rounded-full bg-n-solid-blue"
|
||||
>
|
||||
<Icon
|
||||
class="flex-shrink-0 text-n-brand size-[14px]"
|
||||
icon="i-lucide-lock-keyhole"
|
||||
/>
|
||||
</span>
|
||||
<span class="text-base font-medium text-n-slate-12">
|
||||
{{ $t('GENERAL_SETTINGS.UPGRADE') }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-normal text-n-slate-11 mb-3">
|
||||
{{ limitExceededMessage }}
|
||||
</p>
|
||||
<p v-if="!isAdmin">
|
||||
{{ t('GENERAL_SETTINGS.LIMIT_MESSAGES.NON_ADMIN') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<NextButton
|
||||
v-if="isAdmin"
|
||||
:label="$t('GENERAL_SETTINGS.OPEN_BILLING')"
|
||||
icon="i-lucide-credit-card"
|
||||
@click="routeToBilling()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else />
|
||||
</template>
|
||||
@@ -8,7 +8,7 @@ class AccountPolicy < ApplicationPolicy
|
||||
end
|
||||
|
||||
def limits?
|
||||
@account_user.administrator?
|
||||
@account_user.administrator? || @account_user.agent?
|
||||
end
|
||||
|
||||
def update?
|
||||
|
||||
@@ -13,24 +13,24 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def limits
|
||||
limits = {
|
||||
'conversation' => {},
|
||||
'non_web_inboxes' => {},
|
||||
'captain' => @account.usage_limits[:captain]
|
||||
}
|
||||
|
||||
if default_plan?(@account)
|
||||
limits = {
|
||||
'conversation' => {
|
||||
'allowed' => 500,
|
||||
'consumed' => conversations_this_month(@account)
|
||||
},
|
||||
'non_web_inboxes' => {
|
||||
'allowed' => 0,
|
||||
'consumed' => non_web_inboxes(@account)
|
||||
}
|
||||
}
|
||||
end
|
||||
limits = if default_plan?(@account)
|
||||
{
|
||||
'conversation' => {
|
||||
'allowed' => 500,
|
||||
'consumed' => conversations_this_month(@account)
|
||||
},
|
||||
'non_web_inboxes' => {
|
||||
'allowed' => 0,
|
||||
'consumed' => non_web_inboxes(@account)
|
||||
},
|
||||
'agents' => {
|
||||
'allowed' => 2,
|
||||
'consumed' => agents(@account)
|
||||
}
|
||||
}
|
||||
else
|
||||
default_limits
|
||||
end
|
||||
|
||||
# include id in response to ensure that the store can be updated on the frontend
|
||||
render json: { id: @account.id, limits: limits }, status: :ok
|
||||
@@ -49,6 +49,15 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def default_limits
|
||||
{
|
||||
'conversation' => {},
|
||||
'non_web_inboxes' => {},
|
||||
'agents' => {},
|
||||
'captain' => @account.usage_limits[:captain]
|
||||
}
|
||||
end
|
||||
|
||||
def fetch_account
|
||||
@account = current_user.accounts.find(params[:id])
|
||||
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
|
||||
|
||||
@@ -2,8 +2,8 @@ require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Enterprise Billing APIs', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let!(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let!(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
describe 'POST /enterprise/api/v1/accounts/{account.id}/subscription' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
@@ -121,13 +121,36 @@ RSpec.describe 'Enterprise Billing APIs', type: :request do
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
before do
|
||||
InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_create(value: 'cloud')
|
||||
InstallationConfig.where(name: 'CHATWOOT_CLOUD_PLANS').first_or_create(value: [{ 'name': 'Hacker' }])
|
||||
end
|
||||
|
||||
context 'when it is an agent' do
|
||||
it 'returns unauthorized' do
|
||||
get "/enterprise/api/v1/accounts/#{account.id}/limits",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['id']).to eq(account.id)
|
||||
expect(json_response['limits']).to eq(
|
||||
{
|
||||
'conversation' => {
|
||||
'allowed' => 500,
|
||||
'consumed' => 0
|
||||
},
|
||||
'non_web_inboxes' => {
|
||||
'allowed' => 0,
|
||||
'consumed' => 0
|
||||
},
|
||||
'agents' => {
|
||||
'allowed' => 2,
|
||||
'consumed' => 2
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -155,6 +178,10 @@ RSpec.describe 'Enterprise Billing APIs', type: :request do
|
||||
'non_web_inboxes' => {
|
||||
'allowed' => 0,
|
||||
'consumed' => 1
|
||||
},
|
||||
'agents' => {
|
||||
'allowed' => 2,
|
||||
'consumed' => 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,18 +199,11 @@ RSpec.describe 'Enterprise Billing APIs', type: :request do
|
||||
expected_response = {
|
||||
'id' => account.id,
|
||||
'limits' => {
|
||||
'agents' => {},
|
||||
'conversation' => {},
|
||||
'captain' => {
|
||||
'documents' => {
|
||||
'consumed' => 0,
|
||||
'current_available' => ChatwootApp.max_limit,
|
||||
'total_count' => ChatwootApp.max_limit
|
||||
},
|
||||
'responses' => {
|
||||
'consumed' => 0,
|
||||
'current_available' => ChatwootApp.max_limit,
|
||||
'total_count' => ChatwootApp.max_limit
|
||||
}
|
||||
'documents' => { 'consumed' => 0, 'current_available' => ChatwootApp.max_limit, 'total_count' => ChatwootApp.max_limit },
|
||||
'responses' => { 'consumed' => 0, 'current_available' => ChatwootApp.max_limit, 'total_count' => ChatwootApp.max_limit }
|
||||
},
|
||||
'non_web_inboxes' => {}
|
||||
}
|
||||
@@ -208,6 +228,10 @@ RSpec.describe 'Enterprise Billing APIs', type: :request do
|
||||
'non_web_inboxes' => {
|
||||
'allowed' => 0,
|
||||
'consumed' => 1
|
||||
},
|
||||
'agents' => {
|
||||
'allowed' => 2,
|
||||
'consumed' => 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user