diff --git a/app/javascript/dashboard/api/enterprise/account.js b/app/javascript/dashboard/api/enterprise/account.js index 3f12dc007..69d36fda9 100644 --- a/app/javascript/dashboard/api/enterprise/account.js +++ b/app/javascript/dashboard/api/enterprise/account.js @@ -23,6 +23,35 @@ class EnterpriseAccountAPI extends ApiClient { action_type: action, }); } + + getCreditBalance() { + return axios.get(`${this.url}credits_balance`); + } + + getV2PricingPlans() { + return axios.get(`${this.url}v2_pricing_plans`); + } + + getV2TopupOptions() { + return axios.get(`${this.url}v2_topup_options`); + } + + purchaseTopup(credits) { + return axios.post(`${this.url}v2_topup`, { credits }); + } + + subscribeToV2Plan(pricing_plan_id, quantity = 1) { + return axios.post(`${this.url}v2_subscribe`, { pricing_plan_id, quantity }); + } + + cancelSubscription(options = {}) { + const { feedback = null, comment = null } = options; + + return axios.post(`${this.url}cancel_subscription`, { + feedback, + comment, + }); + } } export default new EnterpriseAccountAPI(); diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 812b0cd8b..150a51efc 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -395,7 +395,92 @@ "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." + "NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again.", + "V2_BILLING": { + "TITLE": "Usage-based billing", + "DESCRIPTION": "Choose the credit package that fits your team, manage usage-based billing, and keep an eye on your credit balance.", + "MANAGE_PAYMENT_METHOD": "Manage payment method", + "MANAGE_BILLING": "Manage billing", + "MANAGE_SUBSCRIPTION": "Manage subscription", + "CONTACT_SUPPORT": "Contact support", + "BACK_TO_BILLING": "Back to Billing", + "CURRENT_PLAN": "Current Plan", + "NO_PLANS_AVAILABLE": "Pricing plans are not available right now. Please try again in a moment or contact support.", + "PLAN_SECTION": { + "TITLE": "Pick the right usage plan", + "DESCRIPTION": "Switch plans anytime - changes take effect immediately and your credits update right away.", + "HELP_TEXT": "Usage beyond the monthly credits is billed at the same overage rate across all plans." + }, + "PLAN_CARD": { + "DEFAULT_UNIT": "credits", + "SUMMARY_WITH_BASE": "{credits} {creditUnit} included per month, {baseFee} base fee, {overage} per additional {creditUnit}.", + "SUMMARY_FREE": "{credits} {creditUnit} included per month with no base fee. Pay {overage} per additional {creditUnit}.", + "HIGHLIGHT_INCLUDED": "{credits} {unit} included every month", + "HIGHLIGHT_OVERAGE": "{rate} per additional {unit}", + "HIGHLIGHT_BASE_FEE": "{amount} monthly base fee", + "HIGHLIGHT_NO_BASE_FEE": "No monthly base fee", + "PRICE_FREE": "Free", + "PER_MONTH": "per month", + "BASE_FEE_LABEL": "Base fee", + "BASE_FEE_VALUE": "{amount} / month", + "BASE_FEE_VALUE_FREE": "No base fee", + "MONTHLY_CREDITS_LABEL": "Monthly credits", + "OVERAGE_RATE_LABEL": "Overage rate", + "OVERAGE_RATE_VALUE": "{rate} / {unit}", + "CTA_SELECT": "Select plan", + "CTA_SWITCH": "Switch plan", + "CTA_CURRENT": "Current plan" + }, + "PLAN_UPDATE": { + "SUCCESS": "You're now on the {plan} plan.", + "ERROR": "We couldn't update the plan to {plan}. Please try again." + }, + "CHECKOUT_SUCCESS": "Subscription successful! Your plan has been updated.", + "CHECKOUT_CANCELLED": "Subscription cancelled. You can try again anytime.", + "ERRORS": { + "FETCH_PLANS": "Failed to load pricing plans. Please refresh and try again.", + "PORTAL": "Failed to open the payment portal. Please try again.", + "SUBSCRIPTION": "We couldn't update your plan. Please try again.", + "TOPUP_OPTIONS": "We couldn't load top-up options. Please try again later.", + "TOPUP": "Top-up failed. Please try again." + }, + "CREDIT_BALANCE": { + "TITLE": "Credit balance", + "DESCRIPTION": "Track monthly and top-up credits available to your account.", + "PLAN_NAME": "Current plan", + "SUBSCRIBED_SEATS": "Subscribed seats", + "MONTHLY_CREDITS": "Monthly credits", + "TOPUP_CREDITS": "Top-up credits", + "TOTAL_AVAILABLE": "Total available", + "USAGE_THIS_MONTH": "Used this month", + "USAGE_TOTAL": "Total usage" + }, + "TOPUP": { + "BUTTON": "Add credits", + "TITLE": "Add top-up credits", + "DESCRIPTION": "Choose a credit pack to increase your available balance immediately.", + "CREDITS_LABEL": "credits", + "INCLUDES": "{amount} billed immediately", + "CANCEL": "Cancel", + "CONFIRM": "Buy credits", + "SUCCESS": "{credits} credits added successfully.", + "ERROR": "We couldn't add {credits} credits. Please try again." + }, + "CANCEL_SUBSCRIPTION": { + "BUTTON_TXT": "Cancel subscription", + "CONFIRM_TITLE": "Cancel subscription", + "CONFIRM_DESCRIPTION": "Are you sure you want to cancel your subscription? Your subscription will remain active until the end of the current billing period.", + "CONFIRM_LABEL": "Yes, cancel subscription", + "CANCEL_LABEL": "No, keep subscription", + "CONFIRM_MESSAGE": "Are you sure you want to cancel your subscription? Your subscription will remain active until the end of the current billing period.", + "CONFIRM_IMMEDIATE_MESSAGE": "Are you sure you want to cancel your subscription immediately? You will lose access to all features immediately.", + "CANCEL_AT_PERIOD_END": "Cancel at period end", + "CANCEL_IMMEDIATELY": "Cancel immediately", + "SUCCESS": "Subscription cancelled successfully. Your subscription will remain active until the end of the current billing period.", + "SUCCESS_WITH_DATE": "Subscription cancelled. You'll retain access until {date}.", + "ERROR": "Failed to cancel subscription" + } + } }, "SECURITY_SETTINGS": { "TITLE": "Security", diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue index 8ba3d69a9..6571acdc9 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue @@ -1,21 +1,26 @@ @@ -139,18 +267,75 @@ onMounted(handleBillingPageLogic); :description="$t('BILLING_SETTINGS.DESCRIPTION')" :link-text="$t('BILLING_SETTINGS.VIEW_PRICING')" feature-name="billing" - /> + > + + + {{ $t('BILLING_SETTINGS.V2_BILLING.TITLE') }} + + + - - {{ $t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.BUTTON_TXT') }} - + + + {{ + isV2Billing + ? $t('BILLING_SETTINGS.V2_BILLING.MANAGE_BILLING') + : $t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.BUTTON_TXT') + }} + + + {{ + $t( + 'BILLING_SETTINGS.V2_BILLING.CANCEL_SUBSCRIPTION.BUTTON_TXT' + ) + }} + + + {{ $t('BILLING_SETTINGS.V2_BILLING.CONTACT_SUPPORT') }} + + + + + + + {{ $t('BILLING_SETTINGS.V2_BILLING.TOPUP.BUTTON') }} + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/V2Billing.vue b/app/javascript/dashboard/routes/dashboard/settings/billing/V2Billing.vue new file mode 100644 index 000000000..94a5a7f0e --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/billing/V2Billing.vue @@ -0,0 +1,790 @@ + + + + + + + + + + + + + + {{ $t('BILLING_SETTINGS.V2_BILLING.BACK_TO_BILLING') }} + + + {{ $t('BILLING_SETTINGS.V2_BILLING.MANAGE_PAYMENT_METHOD') }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ $t('BILLING_SETTINGS.V2_BILLING.TOPUP.BUTTON') }} + + + {{ + $t( + 'BILLING_SETTINGS.V2_BILLING.CANCEL_SUBSCRIPTION.BUTTON_TXT' + ) + }} + + + + + + + + + + {{ $t('BILLING_SETTINGS.V2_BILLING.PLAN_SECTION.TITLE') }} + + + {{ $t('BILLING_SETTINGS.V2_BILLING.PLAN_SECTION.DESCRIPTION') }} + + + + + {{ $t('BILLING_SETTINGS.V2_BILLING.PLAN_SECTION.HELP_TEXT') }} + + + + + + + {{ $t('BILLING_SETTINGS.V2_BILLING.NO_PLANS_AVAILABLE') }} + + + + + + + + + {{ $t('BILLING_SETTINGS.V2_BILLING.CURRENT_PLAN') }} + + + + + Seats: + + setPlanQuantity(plan, e.target.value)" + /> + + + {{ actionLabelForPlan(plan) }} + + + + + + + + + {{ planPriceLabel(plan) }} + + + {{ + $t('BILLING_SETTINGS.V2_BILLING.PLAN_CARD.PER_MONTH') + }} + + + + + + + {{ highlight }} + + + + + + + + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/billing.routes.js b/app/javascript/dashboard/routes/dashboard/settings/billing/billing.routes.js index 8c03257c8..559e39a56 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/billing/billing.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/billing/billing.routes.js @@ -2,6 +2,7 @@ import { frontendURL } from '../../../../helper/URLHelper'; import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes'; import SettingsWrapper from '../SettingsWrapper.vue'; import Index from './Index.vue'; +import V2Billing from './V2Billing.vue'; export default { routes: [ @@ -27,6 +28,15 @@ export default { permissions: ['administrator'], }, }, + { + path: 'v2', + name: 'billing_settings_v2', + component: V2Billing, + meta: { + installationTypes: [INSTALLATION_TYPES.CLOUD], + permissions: ['administrator'], + }, + }, ], }, ], diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingTopupModal.vue b/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingTopupModal.vue new file mode 100644 index 000000000..f7c3abbea --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingTopupModal.vue @@ -0,0 +1,147 @@ + + + + + + + + + + {{ $t('BILLING_SETTINGS.V2_BILLING.TOPUP.TITLE') }} + + + {{ $t('BILLING_SETTINGS.V2_BILLING.TOPUP.DESCRIPTION') }} + + + + + + + + + {{ option.label }} + + + {{ + $t('BILLING_SETTINGS.V2_BILLING.TOPUP.INCLUDES', { + amount: option.price, + }) + }} + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/components/DetailItem.vue b/app/javascript/dashboard/routes/dashboard/settings/billing/components/DetailItem.vue index 1c4f2c458..50461ef9b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/billing/components/DetailItem.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/billing/components/DetailItem.vue @@ -5,7 +5,7 @@ defineProps({ required: true, }, value: { - type: String, + type: [String, Number], required: true, }, }); diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index d56958eb5..f78afcdfd 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -48,6 +48,7 @@ import teamMembers from './modules/teamMembers'; import teams from './modules/teams'; import userNotificationSettings from './modules/userNotificationSettings'; import webhooks from './modules/webhooks'; +import billingV2 from './modules/billingV2'; import captainAssistants from './captain/assistant'; import captainDocuments from './captain/document'; import captainResponses from './captain/response'; @@ -111,6 +112,7 @@ export default createStore({ teams, userNotificationSettings, webhooks, + billingV2, captainAssistants, captainDocuments, captainResponses, diff --git a/app/javascript/dashboard/store/modules/accounts.js b/app/javascript/dashboard/store/modules/accounts.js index 1eef59cfe..4b5487424 100644 --- a/app/javascript/dashboard/store/modules/accounts.js +++ b/app/javascript/dashboard/store/modules/accounts.js @@ -149,6 +149,53 @@ export const actions = { } }, + getCreditBalance: async () => { + const response = await EnterpriseAccountAPI.getCreditBalance(); + return response.data; + }, + + getV2PricingPlans: async ({ commit }) => { + commit(types.default.SET_ACCOUNT_UI_FLAG, { isFetchingItem: true }); + try { + const response = await EnterpriseAccountAPI.getV2PricingPlans(); + commit(types.default.SET_ACCOUNT_UI_FLAG, { isFetchingItem: false }); + return response.data; + } catch (error) { + commit(types.default.SET_ACCOUNT_UI_FLAG, { isFetchingItem: false }); + throwErrorMessage(error); + throw error; + } + }, + + subscribeToV2Plan: async ({ commit }, { pricing_plan_id, quantity = 1 }) => { + commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true }); + try { + const response = await EnterpriseAccountAPI.subscribeToV2Plan( + pricing_plan_id, + quantity + ); + commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }); + return response.data; + } catch (error) { + commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }); + throwErrorMessage(error); + throw error; + } + }, + + cancelSubscription: async ({ commit }, options = {}) => { + commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true }); + try { + const response = await EnterpriseAccountAPI.cancelSubscription(options); + commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }); + return response.data; + } catch (error) { + commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }); + throwErrorMessage(error); + throw error; + } + }, + getCacheKeys: async () => { return AccountAPI.getCacheKeys(); }, diff --git a/app/javascript/dashboard/store/modules/billingV2.js b/app/javascript/dashboard/store/modules/billingV2.js new file mode 100644 index 000000000..072f4f662 --- /dev/null +++ b/app/javascript/dashboard/store/modules/billingV2.js @@ -0,0 +1,68 @@ +import EnterpriseAccountAPI from 'dashboard/api/enterprise/account'; +import { throwErrorMessage } from '../utils/api'; + +const state = { + topupOptions: [], + uiFlags: { + isLoading: false, + isProcessing: false, + }, +}; + +export const getters = { + topupOptions($state) { + return $state.topupOptions; + }, + uiFlags($state) { + return $state.uiFlags; + }, +}; + +export const actions = { + async fetchTopupOptions({ commit }) { + commit('SET_UI_FLAG', { isLoading: true }); + try { + const response = await EnterpriseAccountAPI.getV2TopupOptions(); + commit('SET_TOPUP_OPTIONS', response.data.topup_options); + return response.data.topup_options; + } catch (error) { + throwErrorMessage(error); + throw error; + } finally { + commit('SET_UI_FLAG', { isLoading: false }); + } + }, + + async purchaseTopup({ commit }, { credits }) { + commit('SET_UI_FLAG', { isProcessing: true }); + try { + const response = await EnterpriseAccountAPI.purchaseTopup(credits); + return response.data; + } catch (error) { + throwErrorMessage(error); + throw error; + } finally { + commit('SET_UI_FLAG', { isProcessing: false }); + } + }, +}; + +export const mutations = { + SET_UI_FLAG($state, data) { + $state.uiFlags = { + ...$state.uiFlags, + ...data, + }; + }, + SET_TOPUP_OPTIONS($state, options) { + $state.topupOptions = options || []; + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/package.json b/package.json index 38053b2c3..ccac0b0b4 100644 --- a/package.json +++ b/package.json @@ -79,8 +79,8 @@ "md5": "^2.3.0", "mitt": "^3.0.1", "opus-recorder": "^8.0.5", - "qrcode": "^1.5.4", "posthog-js": "^1.260.2", + "qrcode": "^1.5.4", "semver": "7.6.3", "snakecase-keys": "^8.0.1", "timezone-phone-codes": "^0.0.2",
+ {{ $t('BILLING_SETTINGS.V2_BILLING.PLAN_SECTION.DESCRIPTION') }} +
+ {{ $t('BILLING_SETTINGS.V2_BILLING.PLAN_SECTION.HELP_TEXT') }} +
+ {{ $t('BILLING_SETTINGS.V2_BILLING.TOPUP.DESCRIPTION') }} +
+ {{ option.label }} +
+ {{ + $t('BILLING_SETTINGS.V2_BILLING.TOPUP.INCLUDES', { + amount: option.price, + }) + }} +