Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1a31e50ad |
@@ -6,6 +6,7 @@ class EnterpriseAccountAPI extends ApiClient {
|
||||
super('', { accountScoped: true, enterprise: true });
|
||||
}
|
||||
|
||||
// V1 endpoints
|
||||
checkout() {
|
||||
return axios.post(`${this.url}checkout`);
|
||||
}
|
||||
@@ -23,6 +24,49 @@ class EnterpriseAccountAPI extends ApiClient {
|
||||
action_type: action,
|
||||
});
|
||||
}
|
||||
|
||||
// V2 Billing endpoints
|
||||
get v2BillingUrl() {
|
||||
const accountId = this.accountIdFromRoute;
|
||||
return `/enterprise/api/v2/accounts/${accountId}/billing`;
|
||||
}
|
||||
|
||||
getCreditGrants() {
|
||||
return axios.get(`${this.v2BillingUrl}/credit_grants`);
|
||||
}
|
||||
|
||||
getPricingPlans() {
|
||||
return axios.get(`${this.v2BillingUrl}/pricing_plans`);
|
||||
}
|
||||
|
||||
getTopupOptions() {
|
||||
return axios.get(`${this.v2BillingUrl}/topup_options`);
|
||||
}
|
||||
|
||||
topupCredits(credits) {
|
||||
return axios.post(`${this.v2BillingUrl}/topup`, { credits });
|
||||
}
|
||||
|
||||
subscribeToPlan(pricingPlanId, quantity) {
|
||||
return axios.post(`${this.v2BillingUrl}/subscribe`, {
|
||||
pricing_plan_id: pricingPlanId,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
|
||||
cancelSubscription(reason = null, feedback = null) {
|
||||
return axios.post(`${this.v2BillingUrl}/cancel_subscription`, {
|
||||
reason,
|
||||
feedback,
|
||||
});
|
||||
}
|
||||
|
||||
changePricingPlan(pricingPlanId, quantity) {
|
||||
return axios.post(`${this.v2BillingUrl}/change_pricing_plan`, {
|
||||
pricing_plan_id: pricingPlanId,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new EnterpriseAccountAPI();
|
||||
|
||||
@@ -382,6 +382,8 @@
|
||||
"BILLING_SETTINGS": {
|
||||
"TITLE": "Billing",
|
||||
"DESCRIPTION": "Manage your subscription here, upgrade your plan and get more for your team.",
|
||||
"BILLING_PORTAL": "Billing Portal",
|
||||
"VIEW_ALL_PLANS": "View all plans",
|
||||
"CURRENT_PLAN": {
|
||||
"TITLE": "Current Plan",
|
||||
"PLAN_NOTE": "You are currently subscribed to the **{plan}** plan with **{quantity}** licenses",
|
||||
@@ -394,6 +396,25 @@
|
||||
"DESCRIPTION": "View your previous invoices, edit your billing details, or cancel your subscription.",
|
||||
"BUTTON_TXT": "Go to the billing portal"
|
||||
},
|
||||
"SUBSCRIPTION": {
|
||||
"TITLE": "Subscription",
|
||||
"DESCRIPTION": "Manage your current plan, seats, and billing cycle.",
|
||||
"CURRENT_PLAN": "Current plan",
|
||||
"NUMBER_OF_SEATS": "Number of seats",
|
||||
"RENEWS_ON": "Renews on",
|
||||
"ENDS_ON": "Ends on",
|
||||
"CANCELLED_ON": "Cancelled on",
|
||||
"CHANGE_PLAN": "Change",
|
||||
"CHANGE_SEATS": "Change",
|
||||
"CANCEL_SUBSCRIPTION": "Cancel"
|
||||
},
|
||||
"CAPTAIN_AI": {
|
||||
"TITLE": "Captain AI",
|
||||
"DESCRIPTION": "Purchase and manage credits for Captain AI features.",
|
||||
"AI_CREDITS": "AI credits",
|
||||
"PURCHASE_CREDITS": "Purchase credits",
|
||||
"VIEW_HISTORY": "View credit history"
|
||||
},
|
||||
"CAPTAIN": {
|
||||
"TITLE": "Captain",
|
||||
"DESCRIPTION": "Manage usage and credits for Captain AI.",
|
||||
@@ -402,6 +423,78 @@
|
||||
"RESPONSES": "Responses",
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
|
||||
},
|
||||
"CHANGE_PLAN_MODAL": {
|
||||
"TITLE": "Change plan",
|
||||
"DESCRIPTION": "Select a plan that best fits your needs.",
|
||||
"SUBSCRIBE_TITLE": "Choose a plan",
|
||||
"SUBSCRIBE_DESCRIPTION": "Select a plan to subscribe and unlock all features.",
|
||||
"CURRENT_PLAN": "Current plan",
|
||||
"CREDITS_MONTH": "{credits} credits/month",
|
||||
"PER_USER_MONTH": "/user/month",
|
||||
"AI_CREDITS_MONTH": "AI credits/month",
|
||||
"CANCEL": "Cancel",
|
||||
"SELECT_PLAN": "Select plan",
|
||||
"CHANGE_PLAN": "Change plan",
|
||||
"SUBSCRIBE": "Subscribe"
|
||||
},
|
||||
"CANCEL_MODAL": {
|
||||
"TITLE": "Cancel subscription",
|
||||
"WARNING_TITLE": "You're about to cancel your {plan} plan",
|
||||
"WARNING_DESCRIPTION": "Your subscription will remain active until {date}. After that, you'll lose access to:",
|
||||
"FEATURE_AI_CREDITS": "AI credits for Captain features",
|
||||
"FEATURE_TEAM_COLLABORATION": "Team collaboration tools",
|
||||
"FEATURE_PRIORITY_SUPPORT": "Priority customer support",
|
||||
"KEEP_SUBSCRIPTION": "Keep subscription",
|
||||
"CANCEL_SUBSCRIPTION": "Cancel subscription"
|
||||
},
|
||||
"FEEDBACK_MODAL": {
|
||||
"TITLE": "Help us improve",
|
||||
"DESCRIPTION": "We're sorry to see you go. Please help us improve by sharing your reason for cancelling.",
|
||||
"REASON_LABEL": "Why are you cancelling?",
|
||||
"REASONS": {
|
||||
"TOO_EXPENSIVE": "It's too expensive",
|
||||
"NOT_USING": "I'm not using it enough",
|
||||
"MISSING_FEATURES": "Missing features I need",
|
||||
"BETTER_ALTERNATIVE": "Found a better alternative",
|
||||
"TECHNICAL_ISSUES": "Technical issues",
|
||||
"OTHER": "Other reason"
|
||||
},
|
||||
"FEEDBACK_LABEL": "Additional feedback (optional)",
|
||||
"FEEDBACK_PLACEHOLDER": "Tell us more about your experience...",
|
||||
"GO_BACK": "Go back",
|
||||
"SUBMIT_CANCEL": "Submit and cancel"
|
||||
},
|
||||
"PURCHASE_MODAL": {
|
||||
"TITLE": "Purchase AI credits",
|
||||
"DESCRIPTION": "Choose a credit package that suits your needs.",
|
||||
"MOST_POPULAR": "Most popular",
|
||||
"CREDITS": "credits",
|
||||
"NOTE": "Credits will be added to your account immediately and are non-refundable. Unused credits expire after 12 months.",
|
||||
"CANCEL": "Cancel",
|
||||
"PURCHASE": "Purchase"
|
||||
},
|
||||
"CREDIT_HISTORY": {
|
||||
"TITLE": "Credit history",
|
||||
"DESCRIPTION": "View your credit purchases and allocations.",
|
||||
"NAME": "Name",
|
||||
"CREDITS": "Credits",
|
||||
"SOURCE": "Source",
|
||||
"EFFECTIVE_AT": "Effective at",
|
||||
"EXPIRES_AT": "Expires at",
|
||||
"NO_RECORDS": "No credit history found.",
|
||||
"CLOSE": "Close"
|
||||
},
|
||||
"HELP": {
|
||||
"TITLE": "Need help?",
|
||||
"DESCRIPTION": "Do you face any issues in billing? We are here to help.",
|
||||
"CHAT": "Chat with us"
|
||||
},
|
||||
"ALERTS": {
|
||||
"PLAN_CHANGED": "Your plan has been changed successfully.",
|
||||
"SEATS_UPDATED": "Number of seats updated successfully.",
|
||||
"SUBSCRIPTION_CANCELLED": "Your subscription has been scheduled for cancellation.",
|
||||
"CREDITS_PURCHASED": "Credits purchased successfully."
|
||||
},
|
||||
"CHAT_WITH_US": {
|
||||
"TITLE": "Need help?",
|
||||
"DESCRIPTION": "Do you face any issues in billing? We are here to help.",
|
||||
|
||||
@@ -1,124 +1,252 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useCaptain } from 'dashboard/composables/useCaptain';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { format } from 'date-fns';
|
||||
import sessionStorage from 'shared/helpers/sessionStorage';
|
||||
|
||||
import BillingMeter from './components/BillingMeter.vue';
|
||||
import BillingCard from './components/BillingCard.vue';
|
||||
import BillingHeader from './components/BillingHeader.vue';
|
||||
import DetailItem from './components/DetailItem.vue';
|
||||
import SubscriptionRow from './components/SubscriptionRow.vue';
|
||||
import SeatStepper from './components/SeatStepper.vue';
|
||||
import ChangePlanModal from './components/ChangePlanModal.vue';
|
||||
import CancelSubscriptionModal from './components/CancelSubscriptionModal.vue';
|
||||
import PurchaseCreditsModal from './components/PurchaseCreditsModal.vue';
|
||||
import CreditHistoryModal from './components/CreditHistoryModal.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import ButtonV4 from 'next/button/Button.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { currentAccount, isOnChatwootCloud } = useAccount();
|
||||
const {
|
||||
captainEnabled,
|
||||
captainLimits,
|
||||
documentLimits,
|
||||
responseLimits,
|
||||
fetchLimits,
|
||||
fetchLimits: fetchCaptainLimits,
|
||||
} = useCaptain();
|
||||
|
||||
const uiFlags = useMapGetter('accounts/getUIFlags');
|
||||
const store = useStore();
|
||||
const pricingPlans = useMapGetter('accounts/getPricingPlans');
|
||||
const topupOptions = useMapGetter('accounts/getTopupOptions');
|
||||
const creditGrants = useMapGetter('accounts/getCreditGrants');
|
||||
|
||||
const BILLING_REFRESH_ATTEMPTED = 'billing_refresh_attempted';
|
||||
|
||||
// State for handling refresh attempts and loading
|
||||
// Modal refs
|
||||
const changePlanModalRef = ref(null);
|
||||
const cancelModalRef = ref(null);
|
||||
const purchaseCreditsModalRef = ref(null);
|
||||
const creditHistoryModalRef = ref(null);
|
||||
|
||||
// State
|
||||
const isWaitingForBilling = ref(false);
|
||||
const isEditingSeats = ref(false);
|
||||
const editedSeats = ref(0);
|
||||
|
||||
const customAttributes = computed(() => {
|
||||
return currentAccount.value.custom_attributes || {};
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed property for plan name
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
const planName = computed(() => {
|
||||
return customAttributes.value.plan_name;
|
||||
});
|
||||
const planName = computed(() => customAttributes.value.plan_name);
|
||||
const currentPlanId = computed(
|
||||
() => customAttributes.value.stripe_pricing_plan_id
|
||||
);
|
||||
const subscriptionId = computed(
|
||||
() => customAttributes.value.stripe_subscription_id
|
||||
);
|
||||
const subscribedQuantity = computed(
|
||||
() => customAttributes.value.subscribed_quantity
|
||||
);
|
||||
const subscriptionStatus = computed(
|
||||
() => customAttributes.value.subscription_status
|
||||
);
|
||||
|
||||
/**
|
||||
* Computed property for subscribed quantity
|
||||
* @returns {number|undefined}
|
||||
*/
|
||||
const subscribedQuantity = computed(() => {
|
||||
return customAttributes.value.subscribed_quantity;
|
||||
});
|
||||
// Check if user has an active subscription (required for seat changes and cancellation)
|
||||
const hasActiveSubscription = computed(() => !!subscriptionId.value);
|
||||
|
||||
const subscriptionRenewsOn = computed(() => {
|
||||
if (!customAttributes.value.subscription_ends_on) return '';
|
||||
const endDate = new Date(customAttributes.value.subscription_ends_on);
|
||||
// return date as 12 Jan, 2034
|
||||
const subscriptionEndsAt = computed(() => {
|
||||
if (!customAttributes.value.subscription_ends_at) return '';
|
||||
const endDate = new Date(customAttributes.value.subscription_ends_at);
|
||||
return format(endDate, 'dd MMM, yyyy');
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed property indicating if user has a billing plan
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const hasABillingPlan = computed(() => {
|
||||
return !!planName.value;
|
||||
const subscriptionCancelledAt = computed(() => {
|
||||
if (!customAttributes.value.subscription_cancelled_at) return '';
|
||||
const cancelledDate = new Date(customAttributes.value.subscription_cancelled_at);
|
||||
return format(cancelledDate, 'dd MMM, yyyy');
|
||||
});
|
||||
|
||||
const hasABillingPlan = computed(() => !!planName.value);
|
||||
|
||||
const isCancelled = computed(() => {
|
||||
return subscriptionStatus.value === 'cancel_at_period_end';
|
||||
});
|
||||
|
||||
// Captain credits
|
||||
const creditsUsed = computed(() => {
|
||||
if (!responseLimits.value) return 0;
|
||||
return responseLimits.value.consumed || 0;
|
||||
});
|
||||
|
||||
const creditsTotal = computed(() => {
|
||||
if (!responseLimits.value) return 0;
|
||||
return responseLimits.value.totalCount || 0;
|
||||
});
|
||||
|
||||
// Actions
|
||||
const fetchAccountDetails = async () => {
|
||||
if (!hasABillingPlan.value) {
|
||||
await store.dispatch('accounts/subscription');
|
||||
fetchLimits();
|
||||
}
|
||||
// Always fetch captain limits to show AI credits
|
||||
fetchCaptainLimits();
|
||||
};
|
||||
|
||||
const handleBillingPageLogic = async () => {
|
||||
// If self-hosted, redirect to dashboard
|
||||
if (!isOnChatwootCloud.value) {
|
||||
router.push({ name: 'home' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we've already attempted a refresh for billing setup
|
||||
const billingRefreshAttempted = sessionStorage.get(BILLING_REFRESH_ATTEMPTED);
|
||||
|
||||
// If cloud user, fetch account details first
|
||||
await fetchAccountDetails();
|
||||
|
||||
// If still no billing plan after fetch
|
||||
if (!hasABillingPlan.value) {
|
||||
// If we haven't attempted refresh yet, do it once
|
||||
if (!billingRefreshAttempted) {
|
||||
isWaitingForBilling.value = true;
|
||||
sessionStorage.set(BILLING_REFRESH_ATTEMPTED, true);
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 5000);
|
||||
} else {
|
||||
// We've already tried refreshing, so just show the no billing message
|
||||
// Clear the flag for future visits
|
||||
sessionStorage.remove(BILLING_REFRESH_ATTEMPTED);
|
||||
}
|
||||
} else {
|
||||
// Billing plan found, clear any existing refresh flag
|
||||
sessionStorage.remove(BILLING_REFRESH_ATTEMPTED);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickBillingPortal = () => {
|
||||
const openBillingPortal = () => {
|
||||
store.dispatch('accounts/checkout');
|
||||
};
|
||||
|
||||
const onToggleChatWindow = () => {
|
||||
const openChatWidget = () => {
|
||||
if (window.$chatwoot) {
|
||||
window.$chatwoot.toggle();
|
||||
}
|
||||
};
|
||||
|
||||
// Modal handlers
|
||||
const openChangePlanModal = async () => {
|
||||
await store.dispatch('accounts/fetchPricingPlans');
|
||||
changePlanModalRef.value?.open();
|
||||
};
|
||||
|
||||
const handlePlanSelect = async plan => {
|
||||
try {
|
||||
// If no active subscription, use subscribe API (for Hacker plan users)
|
||||
if (!hasActiveSubscription.value) {
|
||||
await store.dispatch('accounts/subscribeToPlan', {
|
||||
pricingPlanId: plan.id,
|
||||
quantity: 1,
|
||||
});
|
||||
// subscribeToPlan redirects to Stripe, so no need to close modal
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise use change plan API (for existing subscribers)
|
||||
await store.dispatch('accounts/changePricingPlan', {
|
||||
pricingPlanId: plan.id,
|
||||
quantity: subscribedQuantity.value || 1,
|
||||
});
|
||||
changePlanModalRef.value?.close();
|
||||
useAlert(t('BILLING_SETTINGS.ALERTS.PLAN_CHANGED'));
|
||||
await store.dispatch('accounts/subscription');
|
||||
} catch {
|
||||
// Error already handled
|
||||
}
|
||||
};
|
||||
|
||||
// Seats editing
|
||||
const startEditingSeats = () => {
|
||||
editedSeats.value = subscribedQuantity.value || 1;
|
||||
isEditingSeats.value = true;
|
||||
};
|
||||
|
||||
const cancelEditingSeats = () => {
|
||||
isEditingSeats.value = false;
|
||||
};
|
||||
|
||||
const saveSeats = async newSeats => {
|
||||
if (newSeats === subscribedQuantity.value) {
|
||||
isEditingSeats.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await store.dispatch('accounts/changePricingPlan', {
|
||||
pricingPlanId: currentPlanId.value,
|
||||
quantity: newSeats,
|
||||
});
|
||||
isEditingSeats.value = false;
|
||||
useAlert(t('BILLING_SETTINGS.ALERTS.SEATS_UPDATED'));
|
||||
await store.dispatch('accounts/subscription');
|
||||
} catch {
|
||||
// Error already handled
|
||||
}
|
||||
};
|
||||
|
||||
// Cancel subscription
|
||||
const openCancelModal = () => {
|
||||
cancelModalRef.value?.open();
|
||||
};
|
||||
|
||||
const handleCancelConfirm = async ({ reason, feedback }) => {
|
||||
try {
|
||||
await store.dispatch('accounts/cancelAccountSubscription', {
|
||||
reason,
|
||||
feedback,
|
||||
});
|
||||
cancelModalRef.value?.close();
|
||||
useAlert(t('BILLING_SETTINGS.ALERTS.SUBSCRIPTION_CANCELLED'));
|
||||
await store.dispatch('accounts/subscription');
|
||||
} catch {
|
||||
// Error already handled
|
||||
}
|
||||
};
|
||||
|
||||
// Purchase credits
|
||||
const openPurchaseCreditsModal = async () => {
|
||||
await store.dispatch('accounts/fetchTopupOptions');
|
||||
purchaseCreditsModalRef.value?.open();
|
||||
};
|
||||
|
||||
const handlePurchaseCredits = async option => {
|
||||
try {
|
||||
await store.dispatch('accounts/purchaseCredits', {
|
||||
credits: option.credits,
|
||||
});
|
||||
purchaseCreditsModalRef.value?.close();
|
||||
useAlert(t('BILLING_SETTINGS.ALERTS.CREDITS_PURCHASED'));
|
||||
fetchCaptainLimits();
|
||||
} catch {
|
||||
// Error already handled
|
||||
}
|
||||
};
|
||||
|
||||
// Credit history
|
||||
const openCreditHistoryModal = async () => {
|
||||
await store.dispatch('accounts/fetchCreditGrants');
|
||||
creditHistoryModalRef.value?.open();
|
||||
};
|
||||
|
||||
onMounted(handleBillingPageLogic);
|
||||
</script>
|
||||
|
||||
@@ -137,92 +265,187 @@ onMounted(handleBillingPageLogic);
|
||||
<BaseSettingsHeader
|
||||
:title="$t('BILLING_SETTINGS.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS.DESCRIPTION')"
|
||||
:link-text="$t('BILLING_SETTINGS.VIEW_PRICING')"
|
||||
feature-name="billing"
|
||||
/>
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
solid
|
||||
blue
|
||||
sm
|
||||
:label="$t('BILLING_SETTINGS.BILLING_PORTAL')"
|
||||
:is-loading="uiFlags.isCheckoutInProcess"
|
||||
@click="openBillingPortal"
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<section class="grid gap-4">
|
||||
<BillingCard
|
||||
:title="$t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.DESCRIPTION')"
|
||||
<section class="flex flex-col gap-6">
|
||||
<a
|
||||
href="https://www.chatwoot.com/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-n-blue-text hover:underline"
|
||||
>
|
||||
<template #action>
|
||||
<ButtonV4 sm solid blue @click="onClickBillingPortal">
|
||||
{{ $t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.BUTTON_TXT') }}
|
||||
</ButtonV4>
|
||||
</template>
|
||||
<div
|
||||
v-if="planName || subscribedQuantity || subscriptionRenewsOn"
|
||||
class="grid lg:grid-cols-4 sm:grid-cols-3 grid-cols-1 gap-2 divide-x divide-n-weak"
|
||||
>
|
||||
<DetailItem
|
||||
:label="$t('BILLING_SETTINGS.CURRENT_PLAN.TITLE')"
|
||||
{{ $t('BILLING_SETTINGS.VIEW_ALL_PLANS') }}
|
||||
<span class="i-lucide-chevron-right size-4" />
|
||||
</a>
|
||||
|
||||
<!-- Subscription Card -->
|
||||
<BillingCard
|
||||
:title="$t('BILLING_SETTINGS.SUBSCRIPTION.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS.SUBSCRIPTION.DESCRIPTION')"
|
||||
>
|
||||
<div class="px-5">
|
||||
<SubscriptionRow
|
||||
:label="$t('BILLING_SETTINGS.SUBSCRIPTION.CURRENT_PLAN')"
|
||||
:value="planName"
|
||||
:action-text="$t('BILLING_SETTINGS.SUBSCRIPTION.CHANGE_PLAN')"
|
||||
:action-disabled="isCancelled"
|
||||
@action="openChangePlanModal"
|
||||
/>
|
||||
<DetailItem
|
||||
v-if="subscribedQuantity"
|
||||
:label="$t('BILLING_SETTINGS.CURRENT_PLAN.SEAT_COUNT')"
|
||||
|
||||
<SubscriptionRow
|
||||
:label="$t('BILLING_SETTINGS.SUBSCRIPTION.NUMBER_OF_SEATS')"
|
||||
:value="subscribedQuantity"
|
||||
:action-text="$t('BILLING_SETTINGS.SUBSCRIPTION.CHANGE_SEATS')"
|
||||
:show-action="
|
||||
!isEditingSeats && !isCancelled && hasActiveSubscription
|
||||
"
|
||||
@action="startEditingSeats"
|
||||
>
|
||||
<template v-if="isEditingSeats" #value>
|
||||
<SeatStepper
|
||||
v-model="editedSeats"
|
||||
:min="1"
|
||||
:is-loading="uiFlags.isChangingPlan"
|
||||
@save="saveSeats"
|
||||
@cancel="cancelEditingSeats"
|
||||
/>
|
||||
</template>
|
||||
</SubscriptionRow>
|
||||
|
||||
<SubscriptionRow
|
||||
v-if="isCancelled"
|
||||
:label="$t('BILLING_SETTINGS.SUBSCRIPTION.CANCELLED_ON')"
|
||||
:value="subscriptionCancelledAt"
|
||||
/>
|
||||
<DetailItem
|
||||
v-if="subscriptionRenewsOn"
|
||||
:label="$t('BILLING_SETTINGS.CURRENT_PLAN.RENEWS_ON')"
|
||||
:value="subscriptionRenewsOn"
|
||||
|
||||
<SubscriptionRow
|
||||
:label="
|
||||
isCancelled
|
||||
? $t('BILLING_SETTINGS.SUBSCRIPTION.ENDS_ON')
|
||||
: $t('BILLING_SETTINGS.SUBSCRIPTION.RENEWS_ON')
|
||||
"
|
||||
:value="subscriptionEndsAt"
|
||||
:action-text="
|
||||
isCancelled || !hasActiveSubscription
|
||||
? ''
|
||||
: $t('BILLING_SETTINGS.SUBSCRIPTION.CANCEL_SUBSCRIPTION')
|
||||
"
|
||||
@action="openCancelModal"
|
||||
/>
|
||||
</div>
|
||||
</BillingCard>
|
||||
|
||||
<!-- Captain AI Card -->
|
||||
<BillingCard
|
||||
v-if="captainEnabled"
|
||||
:title="$t('BILLING_SETTINGS.CAPTAIN.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS.CAPTAIN.DESCRIPTION')"
|
||||
:title="$t('BILLING_SETTINGS.CAPTAIN_AI.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS.CAPTAIN_AI.DESCRIPTION')"
|
||||
>
|
||||
<template #action>
|
||||
<ButtonV4 sm faded slate disabled>
|
||||
{{ $t('BILLING_SETTINGS.CAPTAIN.BUTTON_TXT') }}
|
||||
</ButtonV4>
|
||||
<Button
|
||||
solid
|
||||
slate
|
||||
sm
|
||||
:label="$t('BILLING_SETTINGS.CAPTAIN_AI.PURCHASE_CREDITS')"
|
||||
@click="openPurchaseCreditsModal"
|
||||
/>
|
||||
</template>
|
||||
<div v-if="captainLimits && responseLimits" class="px-5">
|
||||
<div class="px-5">
|
||||
<BillingMeter
|
||||
:title="$t('BILLING_SETTINGS.CAPTAIN.RESPONSES')"
|
||||
v-bind="responseLimits"
|
||||
:title="$t('BILLING_SETTINGS.CAPTAIN_AI.AI_CREDITS')"
|
||||
:consumed="creditsUsed"
|
||||
:total-count="creditsTotal"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="captainLimits && documentLimits" class="px-5">
|
||||
<BillingMeter
|
||||
:title="$t('BILLING_SETTINGS.CAPTAIN.DOCUMENTS')"
|
||||
v-bind="documentLimits"
|
||||
/>
|
||||
<div class="px-5 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
class="text-sm font-medium text-n-slate-11 hover:text-n-slate-12 hover:underline"
|
||||
@click="openCreditHistoryModal"
|
||||
>
|
||||
{{ $t('BILLING_SETTINGS.CAPTAIN_AI.VIEW_HISTORY') }}
|
||||
</button>
|
||||
</div>
|
||||
</BillingCard>
|
||||
|
||||
<!-- Captain AI Upgrade Card (when not enabled) -->
|
||||
<BillingCard
|
||||
v-else
|
||||
:title="$t('BILLING_SETTINGS.CAPTAIN.TITLE')"
|
||||
:title="$t('BILLING_SETTINGS.CAPTAIN_AI.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS.CAPTAIN.UPGRADE')"
|
||||
>
|
||||
<template #action>
|
||||
<ButtonV4 sm solid slate @click="onClickBillingPortal">
|
||||
{{ $t('CAPTAIN.PAYWALL.UPGRADE_NOW') }}
|
||||
</ButtonV4>
|
||||
<Button
|
||||
solid
|
||||
slate
|
||||
sm
|
||||
:label="$t('CAPTAIN.PAYWALL.UPGRADE_NOW')"
|
||||
@click="openBillingPortal"
|
||||
/>
|
||||
</template>
|
||||
</BillingCard>
|
||||
|
||||
<!-- Help Section -->
|
||||
<BillingHeader
|
||||
class="px-1 mt-5"
|
||||
:title="$t('BILLING_SETTINGS.CHAT_WITH_US.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS.CHAT_WITH_US.DESCRIPTION')"
|
||||
class="px-1 mt-2"
|
||||
:title="$t('BILLING_SETTINGS.HELP.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS.HELP.DESCRIPTION')"
|
||||
>
|
||||
<ButtonV4
|
||||
sm
|
||||
<Button
|
||||
solid
|
||||
slate
|
||||
icon="i-lucide-life-buoy"
|
||||
@click="onToggleChatWindow"
|
||||
>
|
||||
{{ $t('BILLING_SETTINGS.CHAT_WITH_US.BUTTON_TXT') }}
|
||||
</ButtonV4>
|
||||
sm
|
||||
icon="i-lucide-message-circle"
|
||||
:label="$t('BILLING_SETTINGS.HELP.CHAT')"
|
||||
@click="openChatWidget"
|
||||
/>
|
||||
</BillingHeader>
|
||||
</section>
|
||||
|
||||
<!-- Modals -->
|
||||
<ChangePlanModal
|
||||
ref="changePlanModalRef"
|
||||
:plans="pricingPlans"
|
||||
:current-plan-id="currentPlanId"
|
||||
:is-loading="uiFlags.isChangingPlan || uiFlags.isSubscribing"
|
||||
:is-subscribe-mode="!hasActiveSubscription"
|
||||
@select="handlePlanSelect"
|
||||
/>
|
||||
|
||||
<CancelSubscriptionModal
|
||||
ref="cancelModalRef"
|
||||
:plan-name="planName"
|
||||
:renews-on="subscriptionEndsAt"
|
||||
:is-loading="uiFlags.isCancellingSubscription"
|
||||
@confirm="handleCancelConfirm"
|
||||
/>
|
||||
|
||||
<PurchaseCreditsModal
|
||||
ref="purchaseCreditsModalRef"
|
||||
:options="topupOptions"
|
||||
:is-loading="uiFlags.isPurchasingCredits"
|
||||
@purchase="handlePurchaseCredits"
|
||||
/>
|
||||
|
||||
<CreditHistoryModal
|
||||
ref="creditHistoryModalRef"
|
||||
:credit-grants="creditGrants"
|
||||
:is-loading="uiFlags.isFetchingCreditGrants"
|
||||
/>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import CancellationFeedbackForm from './CancellationFeedbackForm.vue';
|
||||
|
||||
defineProps({
|
||||
planName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
renewsOn: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['confirm', 'close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
const step = ref('warning'); // 'warning' | 'feedback'
|
||||
|
||||
const features = [
|
||||
'BILLING_SETTINGS.CANCEL_MODAL.FEATURE_AI_CREDITS',
|
||||
'BILLING_SETTINGS.CANCEL_MODAL.FEATURE_TEAM_COLLABORATION',
|
||||
'BILLING_SETTINGS.CANCEL_MODAL.FEATURE_PRIORITY_SUPPORT',
|
||||
];
|
||||
|
||||
const handleProceedToFeedback = () => {
|
||||
step.value = 'feedback';
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
step.value = 'warning';
|
||||
};
|
||||
|
||||
const handleFeedbackSubmit = ({ reason, feedback }) => {
|
||||
emit('confirm', { reason, feedback });
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
step.value = 'warning';
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
step.value = 'warning';
|
||||
dialogRef.value?.open();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
dialogRef.value?.close();
|
||||
};
|
||||
|
||||
defineExpose({ open, close });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
:title="
|
||||
step === 'warning'
|
||||
? t('BILLING_SETTINGS.CANCEL_MODAL.TITLE')
|
||||
: t('BILLING_SETTINGS.FEEDBACK_MODAL.TITLE')
|
||||
"
|
||||
width="lg"
|
||||
:show-confirm-button="false"
|
||||
:show-cancel-button="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<!-- Warning Step -->
|
||||
<template v-if="step === 'warning'">
|
||||
<div class="p-4 border rounded-lg bg-n-ruby-9/5 border-n-ruby-9/20">
|
||||
<div class="flex gap-3">
|
||||
<span
|
||||
class="flex-shrink-0 i-lucide-alert-triangle size-5 text-n-ruby-9"
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-sm font-medium text-n-ruby-11">
|
||||
{{
|
||||
t('BILLING_SETTINGS.CANCEL_MODAL.WARNING_TITLE', {
|
||||
plan: planName,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p class="text-sm text-n-ruby-11/80">
|
||||
{{
|
||||
t('BILLING_SETTINGS.CANCEL_MODAL.WARNING_DESCRIPTION', {
|
||||
date: renewsOn,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<ul class="mt-2 space-y-1">
|
||||
<li
|
||||
v-for="feature in features"
|
||||
:key="feature"
|
||||
class="flex items-center gap-2 text-sm text-n-ruby-11"
|
||||
>
|
||||
<span class="size-1.5 rounded-full bg-n-ruby-9" />
|
||||
{{ t(feature) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 mt-6">
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="t('BILLING_SETTINGS.CANCEL_MODAL.KEEP_SUBSCRIPTION')"
|
||||
class="flex-1"
|
||||
@click="close"
|
||||
/>
|
||||
<Button
|
||||
color="ruby"
|
||||
solid
|
||||
:label="t('BILLING_SETTINGS.CANCEL_MODAL.CANCEL_SUBSCRIPTION')"
|
||||
class="flex-1"
|
||||
@click="handleProceedToFeedback"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Feedback Step -->
|
||||
<template v-else>
|
||||
<CancellationFeedbackForm
|
||||
:is-loading="isLoading"
|
||||
@submit="handleFeedbackSubmit"
|
||||
@back="handleBack"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps({
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit', 'back']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const selectedReason = ref('');
|
||||
const additionalFeedback = ref('');
|
||||
|
||||
const cancellationReasons = [
|
||||
{
|
||||
key: 'too_expensive',
|
||||
label: 'BILLING_SETTINGS.FEEDBACK_MODAL.REASONS.TOO_EXPENSIVE',
|
||||
},
|
||||
{
|
||||
key: 'not_using',
|
||||
label: 'BILLING_SETTINGS.FEEDBACK_MODAL.REASONS.NOT_USING',
|
||||
},
|
||||
{
|
||||
key: 'missing_features',
|
||||
label: 'BILLING_SETTINGS.FEEDBACK_MODAL.REASONS.MISSING_FEATURES',
|
||||
},
|
||||
{
|
||||
key: 'better_alternative',
|
||||
label: 'BILLING_SETTINGS.FEEDBACK_MODAL.REASONS.BETTER_ALTERNATIVE',
|
||||
},
|
||||
{
|
||||
key: 'technical_issues',
|
||||
label: 'BILLING_SETTINGS.FEEDBACK_MODAL.REASONS.TECHNICAL_ISSUES',
|
||||
},
|
||||
{ key: 'other', label: 'BILLING_SETTINGS.FEEDBACK_MODAL.REASONS.OTHER' },
|
||||
];
|
||||
|
||||
const handleSubmit = () => {
|
||||
emit('submit', {
|
||||
reason: selectedReason.value,
|
||||
feedback: additionalFeedback.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
emit('back');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-sm text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS.FEEDBACK_MODAL.DESCRIPTION') }}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('BILLING_SETTINGS.FEEDBACK_MODAL.REASON_LABEL') }}
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
v-for="reason in cancellationReasons"
|
||||
:key="reason.key"
|
||||
class="flex items-center gap-3 p-3 transition-colors border rounded-lg cursor-pointer border-n-weak hover:bg-n-alpha-1"
|
||||
:class="{
|
||||
'border-n-teal-9 bg-n-teal-9/5': selectedReason === reason.key,
|
||||
}"
|
||||
>
|
||||
<input
|
||||
v-model="selectedReason"
|
||||
type="radio"
|
||||
name="cancellation-reason"
|
||||
:value="reason.key"
|
||||
class="w-4 h-4 accent-n-teal-9"
|
||||
/>
|
||||
<span class="text-sm text-n-slate-12">{{ t(reason.label) }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-sm font-medium text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS.FEEDBACK_MODAL.FEEDBACK_LABEL') }}
|
||||
</label>
|
||||
<textarea
|
||||
v-model="additionalFeedback"
|
||||
rows="3"
|
||||
class="w-full p-3 text-sm transition-colors border rounded-lg resize-none bg-n-alpha-1 border-n-weak text-n-slate-12 placeholder:text-n-slate-10 focus:outline-none focus:border-n-strong"
|
||||
:placeholder="t('BILLING_SETTINGS.FEEDBACK_MODAL.FEEDBACK_PLACEHOLDER')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 px-4 py-2 text-sm font-medium transition-colors border rounded-lg border-n-weak text-n-slate-12 hover:bg-n-alpha-1"
|
||||
:disabled="isLoading"
|
||||
@click="handleBack"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS.FEEDBACK_MODAL.GO_BACK') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 px-4 py-2 text-sm font-medium text-white transition-colors rounded-lg bg-n-slate-9 hover:bg-n-slate-10 disabled:opacity-50"
|
||||
:disabled="!selectedReason || isLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS.FEEDBACK_MODAL.SUBMIT_CANCEL') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import PlanCard from './PlanCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
plans: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
currentPlanId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSubscribeMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select', 'close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
const selectedPlanId = ref(props.currentPlanId);
|
||||
|
||||
const selectedPlan = computed(() => {
|
||||
return filteredPlans.value.find(p => p.id === selectedPlanId.value);
|
||||
});
|
||||
|
||||
// Filter out Hacker plan when user already has a subscription (change plan mode)
|
||||
const filteredPlans = computed(() => {
|
||||
if (props.isSubscribeMode) {
|
||||
// In subscribe mode (no subscription), show all plans including Hacker
|
||||
return props.plans;
|
||||
}
|
||||
// In change plan mode (has subscription), hide Hacker plan
|
||||
return props.plans.filter(
|
||||
plan => plan.display_name?.toLowerCase() !== 'hacker'
|
||||
);
|
||||
});
|
||||
|
||||
const isCurrentPlanSelected = computed(() => {
|
||||
return selectedPlanId.value === props.currentPlanId;
|
||||
});
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
if (props.isSubscribeMode) {
|
||||
return t('BILLING_SETTINGS.CHANGE_PLAN_MODAL.SUBSCRIBE_TITLE');
|
||||
}
|
||||
return t('BILLING_SETTINGS.CHANGE_PLAN_MODAL.TITLE');
|
||||
});
|
||||
|
||||
const modalDescription = computed(() => {
|
||||
if (props.isSubscribeMode) {
|
||||
return t('BILLING_SETTINGS.CHANGE_PLAN_MODAL.SUBSCRIBE_DESCRIPTION');
|
||||
}
|
||||
return t('BILLING_SETTINGS.CHANGE_PLAN_MODAL.DESCRIPTION');
|
||||
});
|
||||
|
||||
const confirmButtonLabel = computed(() => {
|
||||
if (isCurrentPlanSelected.value) {
|
||||
return t('BILLING_SETTINGS.CHANGE_PLAN_MODAL.CURRENT_PLAN');
|
||||
}
|
||||
if (props.isSubscribeMode) {
|
||||
return t('BILLING_SETTINGS.CHANGE_PLAN_MODAL.SUBSCRIBE');
|
||||
}
|
||||
return t('BILLING_SETTINGS.CHANGE_PLAN_MODAL.CHANGE_PLAN');
|
||||
});
|
||||
|
||||
const handlePlanSelect = plan => {
|
||||
selectedPlanId.value = plan.id;
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!isCurrentPlanSelected.value && selectedPlan.value) {
|
||||
emit('select', selectedPlan.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
selectedPlanId.value = props.currentPlanId;
|
||||
dialogRef.value?.open();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
dialogRef.value?.close();
|
||||
};
|
||||
|
||||
defineExpose({ open, close });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
:title="modalTitle"
|
||||
:description="modalDescription"
|
||||
width="2xl"
|
||||
:show-confirm-button="false"
|
||||
:show-cancel-button="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<PlanCard
|
||||
v-for="plan in filteredPlans"
|
||||
:key="plan.id"
|
||||
:plan="plan"
|
||||
:is-selected="selectedPlanId === plan.id"
|
||||
:is-current="plan.id === currentPlanId"
|
||||
:is-disabled="isLoading || plan.id === currentPlanId"
|
||||
@select="handlePlanSelect"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between w-full gap-3">
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="t('BILLING_SETTINGS.CHANGE_PLAN_MODAL.CANCEL')"
|
||||
class="w-full"
|
||||
:disabled="isLoading"
|
||||
@click="close"
|
||||
/>
|
||||
<Button
|
||||
color="slate"
|
||||
solid
|
||||
:label="confirmButtonLabel"
|
||||
class="w-full"
|
||||
:disabled="isCurrentPlanSelected || isLoading"
|
||||
:is-loading="isLoading"
|
||||
@click="handleConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { format } from 'date-fns';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
creditGrants: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
|
||||
const formattedGrants = computed(() => {
|
||||
return props.creditGrants.map(grant => ({
|
||||
...grant,
|
||||
formattedEffectiveAt: grant.effective_at
|
||||
? format(new Date(grant.effective_at), 'dd MMM, yyyy')
|
||||
: '-',
|
||||
formattedExpiresAt: grant.expires_at
|
||||
? format(new Date(grant.expires_at), 'dd MMM, yyyy')
|
||||
: '-',
|
||||
}));
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
dialogRef.value?.open();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
dialogRef.value?.close();
|
||||
};
|
||||
|
||||
defineExpose({ open, close });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
:title="t('BILLING_SETTINGS.CREDIT_HISTORY.TITLE')"
|
||||
:description="t('BILLING_SETTINGS.CREDIT_HISTORY.DESCRIPTION')"
|
||||
width="2xl"
|
||||
:show-confirm-button="false"
|
||||
:show-cancel-button="false"
|
||||
overflow-y-auto
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-8">
|
||||
<span class="i-lucide-loader-2 size-6 animate-spin text-n-slate-11" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="formattedGrants.length === 0" class="py-8 text-center">
|
||||
<span
|
||||
class="i-lucide-credit-card size-12 text-n-slate-9 mx-auto block mb-3"
|
||||
/>
|
||||
<p class="text-sm text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS.CREDIT_HISTORY.NO_RECORDS') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="overflow-hidden border rounded-lg border-n-weak">
|
||||
<table class="w-full">
|
||||
<thead class="bg-n-alpha-1">
|
||||
<tr>
|
||||
<th
|
||||
class="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-n-slate-10"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS.CREDIT_HISTORY.NAME') }}
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-n-slate-10"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS.CREDIT_HISTORY.CREDITS') }}
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-n-slate-10"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS.CREDIT_HISTORY.SOURCE') }}
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-n-slate-10"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS.CREDIT_HISTORY.EFFECTIVE_AT') }}
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-xs font-medium tracking-wider text-left uppercase text-n-slate-10"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS.CREDIT_HISTORY.EXPIRES_AT') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-n-weak">
|
||||
<tr v-for="grant in formattedGrants" :key="grant.id">
|
||||
<td class="px-4 py-3 text-sm text-n-slate-12">
|
||||
{{ grant.name || '-' }}
|
||||
</td>
|
||||
<td
|
||||
class="px-4 py-3 text-sm font-medium tabular-nums text-n-slate-12"
|
||||
>
|
||||
{{ grant.credits?.toLocaleString() || 0 }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-medium rounded bg-n-alpha-2 text-n-slate-11"
|
||||
>
|
||||
{{ grant.source || grant.category || '-' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-n-slate-11">
|
||||
{{ grant.formattedEffectiveAt }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-n-slate-11">
|
||||
{{ grant.formattedExpiresAt }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end w-full">
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="t('BILLING_SETTINGS.CREDIT_HISTORY.CLOSE')"
|
||||
@click="close"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const props = defineProps({
|
||||
credits: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
amount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isPopular: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['select']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const formattedCredits = computed(() => {
|
||||
return props.credits.toLocaleString();
|
||||
});
|
||||
|
||||
const formattedAmount = computed(() => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(props.amount);
|
||||
});
|
||||
|
||||
const cardClasses = computed(() => {
|
||||
const baseClasses =
|
||||
'relative flex flex-col gap-2 p-4 transition-all border rounded-xl cursor-pointer';
|
||||
|
||||
if (props.isSelected) {
|
||||
return `${baseClasses} border-n-teal-9 bg-n-teal-9/5`;
|
||||
}
|
||||
return `${baseClasses} border-n-weak hover:border-n-strong`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cardClasses" @click="$emit('select')">
|
||||
<div
|
||||
v-if="isPopular"
|
||||
class="absolute px-2 py-1 text-xs font-medium text-white rounded -top-3 left-3 bg-n-teal-9"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS.PURCHASE_MODAL.MOST_POPULAR') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="isSelected"
|
||||
class="absolute flex items-center justify-center rounded-full -top-2 -right-2 size-6 bg-n-teal-9"
|
||||
>
|
||||
<span class="text-white i-lucide-check size-4" />
|
||||
</div>
|
||||
<div class="text-2xl font-semibold text-n-slate-12">
|
||||
{{ formattedCredits }}
|
||||
</div>
|
||||
<div class="text-xs font-medium tracking-wider uppercase text-n-slate-10">
|
||||
{{ t('BILLING_SETTINGS.PURCHASE_MODAL.CREDITS') }}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="text-lg font-semibold text-n-slate-12">{{
|
||||
formattedAmount
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const props = defineProps({
|
||||
plan: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isCurrent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['select']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const displayPrice = computed(() => {
|
||||
const licenseFee = props.plan.components?.find(c => c.type === 'license_fee');
|
||||
const amount = licenseFee?.unit_amount || 0;
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(amount);
|
||||
});
|
||||
|
||||
const monthlyCredits = computed(() => {
|
||||
const serviceAction = props.plan.components?.find(
|
||||
c => c.type === 'service_action'
|
||||
);
|
||||
return serviceAction?.credit_amount || 0;
|
||||
});
|
||||
|
||||
const cardClasses = computed(() => {
|
||||
const baseClasses = 'relative flex flex-col gap-2 p-4 transition-all border rounded-xl';
|
||||
|
||||
if (props.isDisabled) {
|
||||
if (props.isSelected) {
|
||||
return `${baseClasses} border-n-teal-9 bg-n-teal-9/5 opacity-70 cursor-not-allowed`;
|
||||
}
|
||||
return `${baseClasses} border-n-weak opacity-50 cursor-not-allowed`;
|
||||
}
|
||||
|
||||
if (props.isSelected) {
|
||||
return `${baseClasses} border-n-teal-9 bg-n-teal-9/5 cursor-pointer`;
|
||||
}
|
||||
return `${baseClasses} border-n-weak hover:border-n-strong cursor-pointer`;
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.isDisabled) {
|
||||
// Emit is handled in template
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cardClasses" @click="!isDisabled && $emit('select', plan)">
|
||||
<div
|
||||
v-if="isCurrent"
|
||||
class="absolute px-2 py-1 text-xs font-medium rounded -top-3 left-3 bg-n-solid-3 text-n-slate-11"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS.CHANGE_PLAN_MODAL.CURRENT_PLAN') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="isSelected"
|
||||
class="absolute flex items-center justify-center rounded-full -top-2 -right-2 size-6 bg-n-teal-9"
|
||||
>
|
||||
<span class="text-white i-lucide-check size-4" />
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-n-slate-12">
|
||||
{{ plan.display_name }}
|
||||
</h3>
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-2xl font-semibold text-n-slate-12">
|
||||
{{ displayPrice }}
|
||||
</span>
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS.CHANGE_PLAN_MODAL.PER_USER_MONTH') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm text-n-slate-11">
|
||||
{{ monthlyCredits }}
|
||||
{{ t('BILLING_SETTINGS.CHANGE_PLAN_MODAL.AI_CREDITS_MONTH') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import CreditPackageCard from './CreditPackageCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['purchase', 'close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
const selectedCredits = ref(null);
|
||||
|
||||
// The 3rd option (5000 credits) is marked as most popular based on Figma
|
||||
const popularCreditsAmount = 5000;
|
||||
|
||||
const selectedOption = computed(() => {
|
||||
return props.options.find(o => o.credits === selectedCredits.value);
|
||||
});
|
||||
|
||||
const handlePackageSelect = credits => {
|
||||
selectedCredits.value = credits;
|
||||
};
|
||||
|
||||
const handlePurchase = () => {
|
||||
if (selectedOption.value) {
|
||||
emit('purchase', selectedOption.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
// Pre-select the most popular option
|
||||
const popularOption = props.options.find(
|
||||
o => o.credits === popularCreditsAmount
|
||||
);
|
||||
selectedCredits.value = popularOption?.credits || props.options[0]?.credits;
|
||||
dialogRef.value?.open();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
dialogRef.value?.close();
|
||||
};
|
||||
|
||||
defineExpose({ open, close });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
:title="t('BILLING_SETTINGS.PURCHASE_MODAL.TITLE')"
|
||||
:description="t('BILLING_SETTINGS.PURCHASE_MODAL.DESCRIPTION')"
|
||||
width="xl"
|
||||
:show-confirm-button="false"
|
||||
:show-cancel-button="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<CreditPackageCard
|
||||
v-for="option in options"
|
||||
:key="option.credits"
|
||||
:credits="option.credits"
|
||||
:amount="option.amount"
|
||||
:is-popular="option.credits === popularCreditsAmount"
|
||||
:is-selected="selectedCredits === option.credits"
|
||||
@select="handlePackageSelect(option.credits)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mt-4 border rounded-lg bg-n-alpha-1 border-n-weak">
|
||||
<p class="text-sm text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS.PURCHASE_MODAL.NOTE') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between w-full gap-3">
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="t('BILLING_SETTINGS.PURCHASE_MODAL.CANCEL')"
|
||||
class="w-full"
|
||||
@click="close"
|
||||
/>
|
||||
<Button
|
||||
color="teal"
|
||||
solid
|
||||
:label="t('BILLING_SETTINGS.PURCHASE_MODAL.PURCHASE')"
|
||||
class="w-full"
|
||||
:disabled="!selectedCredits"
|
||||
:is-loading="isLoading"
|
||||
@click="handlePurchase"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 999,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save', 'cancel']);
|
||||
|
||||
const localValue = ref(props.modelValue);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
newVal => {
|
||||
localValue.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
const decrement = () => {
|
||||
if (localValue.value > props.min) {
|
||||
localValue.value -= 1;
|
||||
emit('update:modelValue', localValue.value);
|
||||
}
|
||||
};
|
||||
|
||||
const increment = () => {
|
||||
if (localValue.value < props.max) {
|
||||
localValue.value += 1;
|
||||
emit('update:modelValue', localValue.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
emit('save', localValue.value);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
localValue.value = props.modelValue;
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="flex items-center overflow-hidden border rounded-lg border-n-weak"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center w-10 h-10 transition-colors text-n-slate-11 hover:bg-n-alpha-2 disabled:opacity-50"
|
||||
:disabled="localValue <= min || isLoading"
|
||||
@click="decrement"
|
||||
>
|
||||
<span class="i-lucide-minus size-4" />
|
||||
</button>
|
||||
<span
|
||||
class="flex items-center justify-center w-12 h-10 text-base font-medium tabular-nums text-n-slate-12"
|
||||
>
|
||||
{{ localValue }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center w-10 h-10 transition-colors text-n-slate-11 hover:bg-n-alpha-2 disabled:opacity-50"
|
||||
:disabled="localValue >= max || isLoading"
|
||||
@click="increment"
|
||||
>
|
||||
<span class="i-lucide-plus size-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="link"
|
||||
color="slate"
|
||||
label="Cancel"
|
||||
:disabled="isLoading"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
<Button
|
||||
solid
|
||||
slate
|
||||
sm
|
||||
label="Save"
|
||||
:is-loading="isLoading"
|
||||
@click="handleSave"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
actionText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
actionDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showAction: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['action']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between py-4 border-b border-n-weak last:border-b-0"
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span
|
||||
class="text-xs font-medium tracking-wider uppercase text-n-slate-10"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
<slot name="value">
|
||||
<span class="text-base font-medium text-n-slate-12">
|
||||
{{ value }}
|
||||
</span>
|
||||
</slot>
|
||||
</div>
|
||||
<slot name="action">
|
||||
<button
|
||||
v-if="actionText && showAction"
|
||||
type="button"
|
||||
class="text-sm font-medium transition-colors text-n-slate-11 hover:text-n-slate-12 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="actionDisabled"
|
||||
@click="$emit('action')"
|
||||
>
|
||||
{{ actionText }}
|
||||
</button>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,7 +18,17 @@ const state = {
|
||||
isFetchingItem: false,
|
||||
isUpdating: false,
|
||||
isCheckoutInProcess: false,
|
||||
isFetchingPricingPlans: false,
|
||||
isFetchingTopupOptions: false,
|
||||
isFetchingCreditGrants: false,
|
||||
isChangingPlan: false,
|
||||
isCancellingSubscription: false,
|
||||
isPurchasingCredits: false,
|
||||
isSubscribing: false,
|
||||
},
|
||||
pricingPlans: [],
|
||||
topupOptions: [],
|
||||
creditGrants: [],
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
@@ -28,6 +38,15 @@ export const getters = {
|
||||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
},
|
||||
getPricingPlans($state) {
|
||||
return $state.pricingPlans;
|
||||
},
|
||||
getTopupOptions($state) {
|
||||
return $state.topupOptions;
|
||||
},
|
||||
getCreditGrants($state) {
|
||||
return $state.creditGrants;
|
||||
},
|
||||
isRTL: ($state, _getters, rootState, rootGetters) => {
|
||||
const accountId = Number(rootState.route?.params?.accountId);
|
||||
const userLocale = rootGetters?.getUISettings?.locale;
|
||||
@@ -152,6 +171,120 @@ export const actions = {
|
||||
getCacheKeys: async () => {
|
||||
return AccountAPI.getCacheKeys();
|
||||
},
|
||||
|
||||
// V2 Billing Actions
|
||||
fetchPricingPlans: async ({ commit }) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isFetchingPricingPlans: true });
|
||||
try {
|
||||
const response = await EnterpriseAccountAPI.getPricingPlans();
|
||||
commit(types.default.SET_PRICING_PLANS, response.data.pricing_plans);
|
||||
} catch (error) {
|
||||
// silent error
|
||||
} finally {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, {
|
||||
isFetchingPricingPlans: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
fetchTopupOptions: async ({ commit }) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isFetchingTopupOptions: true });
|
||||
try {
|
||||
const response = await EnterpriseAccountAPI.getTopupOptions();
|
||||
commit(types.default.SET_TOPUP_OPTIONS, response.data.topup_options);
|
||||
} catch (error) {
|
||||
// silent error
|
||||
} finally {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, {
|
||||
isFetchingTopupOptions: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
fetchCreditGrants: async ({ commit }) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isFetchingCreditGrants: true });
|
||||
try {
|
||||
const response = await EnterpriseAccountAPI.getCreditGrants();
|
||||
commit(types.default.SET_CREDIT_GRANTS, response.data.credit_grants);
|
||||
} catch (error) {
|
||||
// silent error
|
||||
} finally {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, {
|
||||
isFetchingCreditGrants: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
changePricingPlan: async ({ commit }, { pricingPlanId, quantity }) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isChangingPlan: true });
|
||||
try {
|
||||
const response = await EnterpriseAccountAPI.changePricingPlan(
|
||||
pricingPlanId,
|
||||
quantity
|
||||
);
|
||||
commit(types.default.EDIT_ACCOUNT, response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
throw error;
|
||||
} finally {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isChangingPlan: false });
|
||||
}
|
||||
},
|
||||
|
||||
cancelAccountSubscription: async ({ commit }, { reason, feedback }) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, {
|
||||
isCancellingSubscription: true,
|
||||
});
|
||||
try {
|
||||
const response = await EnterpriseAccountAPI.cancelSubscription(
|
||||
reason,
|
||||
feedback
|
||||
);
|
||||
commit(types.default.EDIT_ACCOUNT, response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
throw error;
|
||||
} finally {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, {
|
||||
isCancellingSubscription: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
purchaseCredits: async ({ commit }, { credits }) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isPurchasingCredits: true });
|
||||
try {
|
||||
const response = await EnterpriseAccountAPI.topupCredits(credits);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
throw error;
|
||||
} finally {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isPurchasingCredits: false });
|
||||
}
|
||||
},
|
||||
|
||||
subscribeToPlan: async ({ commit }, { pricingPlanId, quantity }) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isSubscribing: true });
|
||||
try {
|
||||
const response = await EnterpriseAccountAPI.subscribeToPlan(
|
||||
pricingPlanId,
|
||||
quantity
|
||||
);
|
||||
// Redirect to Stripe checkout
|
||||
if (response.data.redirect_url) {
|
||||
window.location = response.data.redirect_url;
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
throw error;
|
||||
} finally {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isSubscribing: false });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
@@ -164,6 +297,15 @@ export const mutations = {
|
||||
[types.default.ADD_ACCOUNT]: MutationHelpers.setSingleRecord,
|
||||
[types.default.EDIT_ACCOUNT]: MutationHelpers.update,
|
||||
[types.default.SET_ACCOUNT_LIMITS]: MutationHelpers.updateAttributes,
|
||||
[types.default.SET_PRICING_PLANS]($state, data) {
|
||||
$state.pricingPlans = data;
|
||||
},
|
||||
[types.default.SET_TOPUP_OPTIONS]($state, data) {
|
||||
$state.topupOptions = data;
|
||||
},
|
||||
[types.default.SET_CREDIT_GRANTS]($state, data) {
|
||||
$state.creditGrants = data;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -76,13 +76,16 @@ export default {
|
||||
EDIT_INBOXES: 'EDIT_INBOXES',
|
||||
DELETE_INBOXES: 'DELETE_INBOXES',
|
||||
|
||||
// Agent
|
||||
// Account
|
||||
SET_ACCOUNT_UI_FLAG: 'SET_ACCOUNT_UI_FLAG',
|
||||
SET_ACCOUNT_LIMITS: 'SET_ACCOUNT_LIMITS',
|
||||
SET_ACCOUNTS: 'SET_ACCOUNTS',
|
||||
ADD_ACCOUNT: 'ADD_ACCOUNT',
|
||||
EDIT_ACCOUNT: 'EDIT_ACCOUNT',
|
||||
DELETE_ACCOUNT: 'DELETE_AGENT',
|
||||
SET_PRICING_PLANS: 'SET_PRICING_PLANS',
|
||||
SET_TOPUP_OPTIONS: 'SET_TOPUP_OPTIONS',
|
||||
SET_CREDIT_GRANTS: 'SET_CREDIT_GRANTS',
|
||||
|
||||
// Agent
|
||||
SET_AGENT_FETCHING_STATUS: 'SET_AGENT_FETCHING_STATUS',
|
||||
|
||||
Reference in New Issue
Block a user