Files
moreminimore-chat/app/javascript/dashboard/helper/routeHelpers.js
Shivam Mishra afa6421809 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>
2026-07-24 14:47:58 +05:30

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';