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 = {
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"M10 13.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5z",
|
||||
"M4 6h1v6.5A2.5 2.5 0 0 0 7.5 15H14v1a2 2 0 0 1-2 2H5.5A3.5 3.5 0 0 1 2 14.5V8a2 2 0 0 1 2-2z"
|
||||
],
|
||||
"credit-card-person-outline": "M2 7.25A3.25 3.25 0 0 1 5.25 4h13.5A3.25 3.25 0 0 1 22 7.25V10h-.258A3.74 3.74 0 0 0 20.5 7.455V7.25a1.75 1.75 0 0 0-1.75-1.75H5.25A1.75 1.75 0 0 0 3.5 7.25v.25h11.95c-.44.409-.782.922-.987 1.5H3.5v5.75c0 .966.784 1.75 1.75 1.75h6.78c.06.522.217 1.028.458 1.5H5.25A3.25 3.25 0 0 1 2 14.75v-7.5Zm21 8.25a1.5 1.5 0 0 0-1.5-1.5h-7a1.5 1.5 0 0 0-1.5 1.5v.5c0 1.971 1.86 4 5 4 3.14 0 5-2.029 5-4v-.5Zm-2.25-5.25a2.75 2.75 0 1 0-5.5 0 2.75 2.75 0 0 0 5.5 0Z",
|
||||
"delete-outline": "M12 1.75a3.25 3.25 0 0 1 3.245 3.066L15.25 5h5.25a.75.75 0 0 1 .102 1.493L20.5 6.5h-.796l-1.28 13.02a2.75 2.75 0 0 1-2.561 2.474l-.176.006H8.313a2.75 2.75 0 0 1-2.714-2.307l-.023-.174L4.295 6.5H3.5a.75.75 0 0 1-.743-.648L2.75 5.75a.75.75 0 0 1 .648-.743L3.5 5h5.25A3.25 3.25 0 0 1 12 1.75Zm6.197 4.75H5.802l1.267 12.872a1.25 1.25 0 0 0 1.117 1.122l.127.006h7.374c.6 0 1.109-.425 1.225-1.002l.02-.126L18.196 6.5ZM13.75 9.25a.75.75 0 0 1 .743.648L14.5 10v7a.75.75 0 0 1-1.493.102L13 17v-7a.75.75 0 0 1 .75-.75Zm-3.5 0a.75.75 0 0 1 .743.648L11 10v7a.75.75 0 0 1-1.493.102L9.5 17v-7a.75.75 0 0 1 .75-.75Zm1.75-6a1.75 1.75 0 0 0-1.744 1.606L10.25 5h3.5A1.75 1.75 0 0 0 12 3.25Z",
|
||||
"dismiss-circle-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17Zm3.446 4.897.084.073a.75.75 0 0 1 .073.976l-.073.084L13.061 12l2.47 2.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-2.47 2.47a.75.75 0 0 1-.976.072l-.084-.073a.75.75 0 0 1-.073-.976l.073-.084L10.939 12l-2.47-2.47a.75.75 0 0 1-.072-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l2.47-2.47a.75.75 0 0 1 .976-.072Z",
|
||||
"dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z",
|
||||
|
||||
@@ -15,6 +15,7 @@ const {
|
||||
TERMS_URL: termsURL,
|
||||
WIDGET_BRAND_URL: widgetBrandURL,
|
||||
DISABLE_USER_PROFILE_UPDATE: disableUserProfileUpdate,
|
||||
DEPLOYMENT_ENV: deploymentEnv,
|
||||
} = window.globalConfig || {};
|
||||
|
||||
const state = {
|
||||
@@ -23,6 +24,7 @@ const state = {
|
||||
appVersion,
|
||||
brandName,
|
||||
chatwootInboxToken,
|
||||
deploymentEnv,
|
||||
createNewAccountFromDashboard,
|
||||
directUploadsEnabled: directUploadsEnabled === 'true',
|
||||
disableUserProfileUpdate: disableUserProfileUpdate === 'true',
|
||||
@@ -38,6 +40,7 @@ const state = {
|
||||
|
||||
export const getters = {
|
||||
get: $state => $state,
|
||||
isOnChatwootCloud: $state => $state.deploymentEnv === 'cloud',
|
||||
};
|
||||
|
||||
export const actions = {};
|
||||
|
||||
Reference in New Issue
Block a user