feat: allow suspended accounts to access billing (#15153)

Administrators of suspended accounts can now reach the billing page to
settle payment and restore their account, instead of being fully locked
out on the suspended screen. The suspended screen shows a subtle
**Manage billing** link below the contact support button (admins on
Cloud only). Agents remain restricted to the suspended screen.

## How to test

1. On Cloud, suspend an account from Super Admin.
2. Log in as an administrator of that account — you land on the
suspended screen with a "Manage billing" link below contact support.
3. Click it — the billing settings page opens; subscription, Stripe
portal, and top-ups all work.
4. Log in as an agent — no billing link, and navigating to
`/settings/billing` manually redirects back to the suspended screen.

## What changed

- Router guard allows `billing_settings_index` for administrators when
the account is not active.
- Suspended screen gets a billing link gated by admin role and Cloud
installation.

<img width="3024" height="1718" alt="CleanShot 2026-07-24 at 12 25
56@2x"
src="https://github.com/user-attachments/assets/1905d070-5e67-4283-9c8d-6e9936bba9a3"
/>

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Shivam Mishra
2026-07-24 14:47:58 +05:30
committed by GitHub
parent 65499df6a3
commit afa6421809
3 changed files with 29 additions and 5 deletions

View File

@@ -67,8 +67,14 @@ export const validateLoggedInRoutes = (to, user) => {
return validateActiveAccountRoutes(to, user);
}
// If the current account is not active, then redirect the user to the suspended screen
if (to.name !== 'account_suspended') {
// If the current account is not active, only the suspended screen is
// reachable; administrators can also access billing to restore the account
const userPermissions = getUserPermissions(user, to.params.accountId);
const accessibleRoutes = userPermissions.includes('administrator')
? ['account_suspended', 'billing_settings_index']
: ['account_suspended'];
if (!accessibleRoutes.includes(to.name)) {
return `accounts/${to.params.accountId}/suspended`;
}

View File

@@ -264,7 +264,8 @@
"EMAIL_VERIFICATION_SENT": "Verification email has been sent. Please check your inbox.",
"ACCOUNT_SUSPENDED": {
"TITLE": "Account Suspended",
"MESSAGE": "Your account has been suspended due to activity that may violate our policies. If you believe this is a mistake, please contact our support team."
"MESSAGE": "Your account has been suspended due to activity that may violate our policies. If you believe this is a mistake, please contact our support team.",
"MANAGE_BILLING": "Manage billing"
},
"NO_ACCOUNTS": {
"TITLE": "No account found",

View File

@@ -1,7 +1,16 @@
<script setup>
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
import { onMounted } from 'vue';
import { computed, onMounted } from 'vue';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { useMapGetter } from 'dashboard/composables/store';
const { isAdmin } = useAdmin();
const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
const showBillingLink = computed(
() => isAdmin.value && isOnChatwootCloud.value
);
const toggleSupportWidgetVisibility = () => {
if (window.$chatwoot) {
@@ -30,15 +39,23 @@ onMounted(() => {
<template>
<div class="items-center bg-n-slate-2 flex justify-center h-full w-full">
<EmptyState
class="max-w-lg"
:title="$t('APP_GLOBAL.ACCOUNT_SUSPENDED.TITLE')"
:message="$t('APP_GLOBAL.ACCOUNT_SUSPENDED.MESSAGE')"
>
<div class="flex justify-center">
<div class="flex flex-col items-center gap-3 mt-4">
<NextButton
icon="i-lucide-life-buoy"
:label="$t('SIDEBAR_ITEMS.CONTACT_SUPPORT')"
@click="toggleSupportWidget"
/>
<router-link
v-if="showBillingLink"
:to="{ name: 'billing_settings_index' }"
class="text-sm text-n-slate-11 hover:text-n-slate-12 hover:underline"
>
{{ $t('APP_GLOBAL.ACCOUNT_SUSPENDED.MANAGE_BILLING') }}
</router-link>
</div>
</EmptyState>
</div>