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>
153 lines
4.6 KiB
JavaScript
153 lines
4.6 KiB
JavaScript
import {
|
|
hasPermissions,
|
|
getUserPermissions,
|
|
getCurrentAccount,
|
|
} from './permissionsHelper';
|
|
|
|
import {
|
|
ROLES,
|
|
CONVERSATION_PERMISSIONS,
|
|
CONTACT_PERMISSIONS,
|
|
REPORTS_PERMISSIONS,
|
|
PORTAL_PERMISSIONS,
|
|
} from 'dashboard/constants/permissions.js';
|
|
|
|
export const routeIsAccessibleFor = (route, userPermissions = []) => {
|
|
const { meta: { permissions: routePermissions = [] } = {} } = route;
|
|
return hasPermissions(routePermissions, userPermissions);
|
|
};
|
|
|
|
export const defaultRedirectPage = (to, permissions) => {
|
|
const { accountId } = to.params;
|
|
|
|
const permissionRoutes = [
|
|
{
|
|
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
|
path: 'dashboard',
|
|
},
|
|
{ permissions: [CONTACT_PERMISSIONS], path: 'contacts' },
|
|
{ permissions: [REPORTS_PERMISSIONS], path: 'reports/overview' },
|
|
{ permissions: [PORTAL_PERMISSIONS], path: 'portals' },
|
|
];
|
|
|
|
const route = permissionRoutes.find(({ permissions: routePermissions }) =>
|
|
hasPermissions(routePermissions, permissions)
|
|
);
|
|
|
|
return `accounts/${accountId}/${route ? route.path : 'dashboard'}`;
|
|
};
|
|
|
|
const validateActiveAccountRoutes = (to, user) => {
|
|
// If the current account is active, then check for the route permissions
|
|
const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`;
|
|
|
|
// If the user is trying to access suspended route, redirect them to dashboard
|
|
if (to.name === 'account_suspended') {
|
|
return accountDashboardURL;
|
|
}
|
|
|
|
const userPermissions = getUserPermissions(user, to.params.accountId);
|
|
|
|
const isAccessible = routeIsAccessibleFor(to, userPermissions);
|
|
// If the route is not accessible for the user, return to dashboard screen
|
|
return isAccessible ? null : defaultRedirectPage(to, userPermissions);
|
|
};
|
|
|
|
export const validateLoggedInRoutes = (to, user) => {
|
|
const currentAccount = getCurrentAccount(user, Number(to.params.accountId));
|
|
// If current account is missing, either user does not have
|
|
// access to the account or the account is deleted, return to login screen
|
|
if (!currentAccount) {
|
|
return `app/login`;
|
|
}
|
|
|
|
const isCurrentAccountActive = currentAccount.status === 'active';
|
|
|
|
if (isCurrentAccountActive) {
|
|
return validateActiveAccountRoutes(to, user);
|
|
}
|
|
|
|
// 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`;
|
|
}
|
|
|
|
// Proceed to the route if none of the above conditions are met
|
|
return null;
|
|
};
|
|
|
|
export const isAConversationRoute = (
|
|
routeName,
|
|
includeBase = false,
|
|
includeExtended = true
|
|
) => {
|
|
const baseRoutes = [
|
|
'home',
|
|
'conversation_mentions',
|
|
'conversation_unattended',
|
|
'inbox_dashboard',
|
|
'label_conversations',
|
|
'team_conversations',
|
|
'folder_conversations',
|
|
'conversation_participating',
|
|
];
|
|
const extendedRoutes = [
|
|
'inbox_conversation',
|
|
'conversation_through_mentions',
|
|
'conversation_through_unattended',
|
|
'conversation_through_inbox',
|
|
'conversations_through_label',
|
|
'conversations_through_team',
|
|
'conversations_through_folders',
|
|
'conversation_through_participating',
|
|
];
|
|
|
|
const routes = [
|
|
...(includeBase ? baseRoutes : []),
|
|
...(includeExtended ? extendedRoutes : []),
|
|
];
|
|
|
|
return routes.includes(routeName);
|
|
};
|
|
|
|
export const getConversationDashboardRoute = routeName => {
|
|
switch (routeName) {
|
|
case 'inbox_conversation':
|
|
return 'home';
|
|
case 'conversation_through_mentions':
|
|
return 'conversation_mentions';
|
|
case 'conversation_through_unattended':
|
|
return 'conversation_unattended';
|
|
case 'conversations_through_label':
|
|
return 'label_conversations';
|
|
case 'conversations_through_team':
|
|
return 'team_conversations';
|
|
case 'conversations_through_folders':
|
|
return 'folder_conversations';
|
|
case 'conversation_through_participating':
|
|
return 'conversation_participating';
|
|
case 'conversation_through_inbox':
|
|
return 'inbox_dashboard';
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export const isAInboxViewRoute = (routeName, includeBase = false) => {
|
|
const baseRoutes = ['inbox_view'];
|
|
const extendedRoutes = ['inbox_view_conversation'];
|
|
const routeNames = includeBase
|
|
? [...baseRoutes, ...extendedRoutes]
|
|
: extendedRoutes;
|
|
return routeNames.includes(routeName);
|
|
};
|
|
|
|
export const isNotificationRoute = routeName =>
|
|
routeName === 'notifications_index';
|