feat: Allow SaaS users to manage subscription within the dashboard (#5059)
This commit is contained in:
18
app/javascript/dashboard/api/enterprise/account.js
Normal file
18
app/javascript/dashboard/api/enterprise/account.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class EnterpriseAccountAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('', { accountScoped: true, enterprise: true });
|
||||
}
|
||||
|
||||
checkout() {
|
||||
return axios.post(`${this.url}checkout`);
|
||||
}
|
||||
|
||||
subscription() {
|
||||
return axios.post(`${this.url}subscription`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new EnterpriseAccountAPI();
|
||||
@@ -0,0 +1,31 @@
|
||||
import accountAPI from '../account';
|
||||
import ApiClient from '../../ApiClient';
|
||||
import describeWithAPIMock from '../../specs/apiSpecHelper';
|
||||
|
||||
describe('#enterpriseAccountAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(accountAPI).toBeInstanceOf(ApiClient);
|
||||
expect(accountAPI).toHaveProperty('get');
|
||||
expect(accountAPI).toHaveProperty('show');
|
||||
expect(accountAPI).toHaveProperty('create');
|
||||
expect(accountAPI).toHaveProperty('update');
|
||||
expect(accountAPI).toHaveProperty('delete');
|
||||
expect(accountAPI).toHaveProperty('checkout');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#checkout', () => {
|
||||
accountAPI.checkout();
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/enterprise/api/v1/checkout'
|
||||
);
|
||||
});
|
||||
|
||||
it('#subscription', () => {
|
||||
accountAPI.subscription();
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/enterprise/api/v1/subscription'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,7 @@
|
||||
:custom-views="customViews"
|
||||
:menu-config="activeSecondaryMenu"
|
||||
:current-role="currentRole"
|
||||
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
||||
@add-label="showAddLabelPopup"
|
||||
@toggle-accounts="toggleAccountModal"
|
||||
/>
|
||||
@@ -67,6 +68,7 @@ export default {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
accountId: 'getCurrentAccountId',
|
||||
currentRole: 'getCurrentRole',
|
||||
|
||||
@@ -30,6 +30,7 @@ const settings = accountId => ({
|
||||
'settings_teams_edit',
|
||||
'settings_teams_edit_members',
|
||||
'settings_teams_edit_finish',
|
||||
'billing_settings_index',
|
||||
'automation_list',
|
||||
],
|
||||
menuItems: [
|
||||
@@ -100,6 +101,14 @@ const settings = accountId => ({
|
||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||
toStateName: 'settings_applications',
|
||||
},
|
||||
{
|
||||
icon: 'credit-card-person',
|
||||
label: 'BILLING',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/billing`),
|
||||
toStateName: 'billing_settings_index',
|
||||
showOnlyOnCloud: true,
|
||||
},
|
||||
{
|
||||
icon: 'settings',
|
||||
label: 'ACCOUNT_SETTINGS',
|
||||
|
||||
@@ -55,6 +55,10 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isOnChatwootCloud: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasSecondaryMenu() {
|
||||
@@ -67,12 +71,18 @@ export default {
|
||||
if (!this.currentRole) {
|
||||
return [];
|
||||
}
|
||||
return this.menuConfig.menuItems.filter(
|
||||
const menuItemsFilteredByRole = this.menuConfig.menuItems.filter(
|
||||
menuItem =>
|
||||
window.roleWiseRoutes[this.currentRole].indexOf(
|
||||
menuItem.toStateName
|
||||
) > -1
|
||||
);
|
||||
return menuItemsFilteredByRole.filter(item => {
|
||||
if (item.showOnlyOnCloud) {
|
||||
return this.isOnChatwootCloud;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
inboxSection() {
|
||||
return {
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
"CUSTOM_ATTRIBUTES": "Custom Attributes",
|
||||
"AUTOMATION": "Automation",
|
||||
"TEAMS": "Teams",
|
||||
"BILLING": "Billing",
|
||||
"CUSTOM_VIEWS_FOLDER": "Folders",
|
||||
"CUSTOM_VIEWS_SEGMENTS": "Segments",
|
||||
"ALL_CONTACTS": "All Contacts",
|
||||
@@ -195,6 +196,25 @@
|
||||
"CATEGORY": "Category"
|
||||
}
|
||||
},
|
||||
"BILLING_SETTINGS": {
|
||||
"TITLE": "Billing",
|
||||
"CURRENT_PLAN" : {
|
||||
"TITLE": "Current Plan",
|
||||
"PLAN_NOTE": "You are currently subscribed to the **%{plan}** plan with **%{quantity}** licenses"
|
||||
},
|
||||
"MANAGE_SUBSCRIPTION": {
|
||||
"TITLE": "Manage your subscription",
|
||||
"DESCRIPTION": "View your previous invoices, edit your billing details, or cancel your subscription.",
|
||||
"BUTTON_TXT": "Go to the billing portal"
|
||||
},
|
||||
|
||||
"CHAT_WITH_US": {
|
||||
"TITLE": "Need help?",
|
||||
"DESCRIPTION": "Do you face any issues in billing? We are here to help.",
|
||||
"BUTTON_TXT": "Chat with us"
|
||||
},
|
||||
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again."
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
||||
"NEW_ACCOUNT": "New Account",
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="columns profile--settings">
|
||||
<woot-loading-state v-if="uiFlags.isFetchingItem" />
|
||||
<div v-else-if="!hasABillingPlan">
|
||||
<p>{{ $t('BILLING_SETTINGS.NO_BILLING_USER') }}</p>
|
||||
</div>
|
||||
<div v-else class="small-12 columns ">
|
||||
<div class="current-plan--details">
|
||||
<h6>{{ $t('BILLING_SETTINGS.CURRENT_PLAN.TITLE') }}</h6>
|
||||
<div
|
||||
v-dompurify-html="
|
||||
formatMessage(
|
||||
$t('BILLING_SETTINGS.CURRENT_PLAN.PLAN_NOTE', {
|
||||
plan: planName,
|
||||
quantity: subscribedQuantity,
|
||||
})
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<billing-item
|
||||
:title="$t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.DESCRIPTION')"
|
||||
:button-label="$t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.BUTTON_TXT')"
|
||||
@click="onClickBillingPortal"
|
||||
/>
|
||||
<billing-item
|
||||
:title="$t('BILLING_SETTINGS.CHAT_WITH_US.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS.CHAT_WITH_US.DESCRIPTION')"
|
||||
:button-label="$t('BILLING_SETTINGS.CHAT_WITH_US.BUTTON_TXT')"
|
||||
button-icon="chat-multiple"
|
||||
@click="onToggleChatWindow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import accountMixin from '../../../../mixins/account';
|
||||
import BillingItem from './components/BillingItem.vue';
|
||||
|
||||
export default {
|
||||
components: { BillingItem },
|
||||
mixins: [accountMixin, alertMixin, messageFormatterMixin],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAccount: 'accounts/getAccount',
|
||||
uiFlags: 'accounts/getUIFlags',
|
||||
}),
|
||||
currentAccount() {
|
||||
return this.getAccount(this.accountId) || {};
|
||||
},
|
||||
customAttributes() {
|
||||
return this.currentAccount.custom_attributes || {};
|
||||
},
|
||||
hasABillingPlan() {
|
||||
return !!this.planName;
|
||||
},
|
||||
planName() {
|
||||
return this.customAttributes.plan_name || '';
|
||||
},
|
||||
subscribedQuantity() {
|
||||
return this.customAttributes.subscribed_quantity || 0;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchAccountDetails();
|
||||
},
|
||||
methods: {
|
||||
async fetchAccountDetails() {
|
||||
await this.$store.dispatch('accounts/get');
|
||||
|
||||
if (!this.hasABillingPlan) {
|
||||
this.$store.dispatch('accounts/subscription');
|
||||
}
|
||||
},
|
||||
onClickBillingPortal() {
|
||||
this.$store.dispatch('accounts/checkout');
|
||||
},
|
||||
onToggleChatWindow() {
|
||||
if (window.$chatwoot) {
|
||||
window.$chatwoot.toggle();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.manage-subscription {
|
||||
align-items: center;
|
||||
background: var(--white);
|
||||
border-radius: var(--border-radius-normal);
|
||||
border: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--space-small);
|
||||
padding: var(--space-medium) var(--space-normal);
|
||||
}
|
||||
|
||||
.current-plan--details {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: var(--space-normal);
|
||||
padding-bottom: var(--space-normal);
|
||||
}
|
||||
|
||||
.manage-subscription {
|
||||
.manage-subscription--description {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
import SettingsContent from '../Wrapper';
|
||||
import Index from './Index.vue';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/billing'),
|
||||
roles: ['administrator'],
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'BILLING_SETTINGS.TITLE',
|
||||
icon: 'credit-card-person',
|
||||
showNewButton: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'billing_settings_index',
|
||||
component: Index,
|
||||
roles: ['administrator'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="manage-subscription">
|
||||
<div>
|
||||
<h6>{{ title }}</h6>
|
||||
<p class="manage-subscription--description">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<woot-button variant="smooth" :icon="buttonIcon" @click="$emit('click')">
|
||||
{{ buttonLabel }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
buttonIcon: {
|
||||
type: String,
|
||||
default: 'edit',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -13,6 +13,7 @@ import teams from './teams/teams.routes';
|
||||
import attributes from './attributes/attributes.routes';
|
||||
import automation from './automation/automation.routes';
|
||||
import store from '../../../store';
|
||||
import billing from './billing/billing.routes';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
@@ -29,16 +30,17 @@ export default {
|
||||
},
|
||||
...account.routes,
|
||||
...agent.routes,
|
||||
...attributes.routes,
|
||||
...automation.routes,
|
||||
...billing.routes,
|
||||
...campaigns.routes,
|
||||
...canned.routes,
|
||||
...inbox.routes,
|
||||
...integrationapps.routes,
|
||||
...integrations.routes,
|
||||
...labels.routes,
|
||||
...profile.routes,
|
||||
...reports.routes,
|
||||
...teams.routes,
|
||||
...campaigns.routes,
|
||||
...integrationapps.routes,
|
||||
...attributes.routes,
|
||||
...automation.routes,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/* eslint no-console: 0 */
|
||||
/* eslint no-param-reassign: 0 */
|
||||
/* eslint no-shadow: 0 */
|
||||
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||
import * as types from '../mutation-types';
|
||||
import AccountAPI from '../../api/account';
|
||||
import EnterpriseAccountAPI from '../../api/enterprise/account';
|
||||
import { throwErrorMessage } from '../utils/api';
|
||||
|
||||
const state = {
|
||||
records: [],
|
||||
@@ -11,6 +10,7 @@ const state = {
|
||||
isFetching: false,
|
||||
isFetchingItem: false,
|
||||
isUpdating: false,
|
||||
isCheckoutInProcess: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -60,6 +60,29 @@ export const actions = {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
checkout: async ({ commit }) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCheckoutInProcess: true });
|
||||
try {
|
||||
const response = await EnterpriseAccountAPI.checkout();
|
||||
window.location = response.data.redirect_url;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCheckoutInProcess: false });
|
||||
}
|
||||
},
|
||||
|
||||
subscription: async ({ commit }) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCheckoutInProcess: true });
|
||||
try {
|
||||
await EnterpriseAccountAPI.subscription();
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCheckoutInProcess: false });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
|
||||
Reference in New Issue
Block a user