feat: Allow SaaS users to manage subscription within the dashboard (#5059)

This commit is contained in:
Pranav Raj S
2022-07-19 19:04:17 +05:30
committed by GitHub
parent 0cee42a9f9
commit 7fc0d166e8
33 changed files with 773 additions and 18 deletions

View 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();

View File

@@ -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'
);
});
});
});

View File

@@ -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',

View File

@@ -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',

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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>

View File

@@ -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'],
},
],
},
],
};

View File

@@ -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>

View File

@@ -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,
],
};

View File

@@ -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 = {