feat(billing): let accounts switch billing currency with a confirmation note

This commit is contained in:
Tanmay Deep Sharma
2026-06-03 11:27:25 +05:30
parent bbd575d8fa
commit f1bc3208e9
13 changed files with 384 additions and 2 deletions
@@ -32,6 +32,10 @@ class EnterpriseAccountAPI extends ApiClient {
getTopupOptions() {
return axios.get(`${this.url}topup_options`);
}
switchCurrency(currency) {
return axios.post(`${this.url}switch_currency`, { currency });
}
}
export default new EnterpriseAccountAPI();
@@ -447,6 +447,23 @@
"DESCRIPTION": "View your previous invoices, edit your billing details, or cancel your subscription.",
"BUTTON_TXT": "Go to the billing portal"
},
"CURRENCY": {
"TITLE": "Billing currency",
"DESCRIPTION": "Choose the currency used for your subscription and credit purchases.",
"OPTIONS": {
"USD": "US Dollar (USD)",
"BRL": "Brazilian Real (BRL)"
},
"SUCCESS": "Billing currency updated successfully.",
"ERROR": "Failed to update billing currency. Please try again.",
"CONFIRM": {
"TITLE": "Switch billing currency?",
"DESCRIPTION": "You are about to switch your billing currency to {currency}.",
"WARNING": "Your current subscription will be cancelled and a new one will be started in the selected currency. Pricing and any remaining credits may differ. This cannot be undone automatically.",
"CONFIRM_BUTTON": "Switch currency",
"CANCEL_BUTTON": "Cancel"
}
},
"CAPTAIN": {
"TITLE": "Captain",
"DESCRIPTION": "Manage usage and credits for Captain AI.",
@@ -1,22 +1,32 @@
<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 {
SUPPORTED_BILLING_CURRENCIES,
DEFAULT_BILLING_CURRENCY,
getCurrencyConfig,
} from 'dashboard/constants/billing';
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 PurchaseCreditsModal from './components/PurchaseCreditsModal.vue';
import SwitchCurrencyDialog from './components/SwitchCurrencyDialog.vue';
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
import SettingsLayout from '../SettingsLayout.vue';
import TabBar from 'dashboard/components-next/tabbar/TabBar.vue';
import ButtonV4 from 'next/button/Button.vue';
const router = useRouter();
const { t } = useI18n();
const { currentAccount, isOnChatwootCloud } = useAccount();
const {
captainEnabled,
@@ -118,6 +128,55 @@ const handleBillingPageLogic = async () => {
}
};
const isSwitchingCurrency = computed(() => uiFlags.value.isSwitchingCurrency);
// Currency switching is rolled out to Brazil (pt_BR) accounts only for now.
const showCurrencyToggle = computed(
() => currentAccount.value?.locale === 'pt_BR'
);
const currentBillingCurrency = computed(() =>
(
customAttributes.value.billing_currency || DEFAULT_BILLING_CURRENCY
).toLowerCase()
);
const currencyTabs = computed(() =>
SUPPORTED_BILLING_CURRENCIES.map(code => ({
label: t(getCurrencyConfig(code).i18nLabelKey),
value: code,
}))
);
const activeCurrencyIndex = computed(() => {
const index = SUPPORTED_BILLING_CURRENCIES.indexOf(
currentBillingCurrency.value
);
return index === -1 ? 0 : index;
});
const pendingCurrency = ref(null);
const switchCurrencyDialogRef = ref(null);
const onSelectCurrency = tab => {
if (!tab?.value || tab.value === currentBillingCurrency.value) return;
if (isSwitchingCurrency.value) return;
pendingCurrency.value = tab.value;
switchCurrencyDialogRef.value?.open();
};
const onConfirmSwitchCurrency = async () => {
try {
await store.dispatch('accounts/switchBillingCurrency', {
currency: pendingCurrency.value,
});
switchCurrencyDialogRef.value?.close();
useAlert(t('BILLING_SETTINGS.CURRENCY.SUCCESS'));
} catch (error) {
useAlert(error.message || t('BILLING_SETTINGS.CURRENCY.ERROR'));
}
};
const onClickBillingPortal = () => {
store.dispatch('accounts/checkout');
};
@@ -190,6 +249,23 @@ onMounted(handleBillingPageLogic);
/>
</div>
</BillingCard>
<BillingCard
v-if="showCurrencyToggle"
:title="$t('BILLING_SETTINGS.CURRENCY.TITLE')"
:description="$t('BILLING_SETTINGS.CURRENCY.DESCRIPTION')"
>
<template #action>
<div
:class="{ 'pointer-events-none opacity-60': isSwitchingCurrency }"
>
<TabBar
:tabs="currencyTabs"
:initial-active-tab="activeCurrencyIndex"
@tab-changed="onSelectCurrency"
/>
</div>
</template>
</BillingCard>
<BillingCard
v-if="captainEnabled"
:title="$t('BILLING_SETTINGS.CAPTAIN.TITLE')"
@@ -263,6 +339,12 @@ onMounted(handleBillingPageLogic);
ref="purchaseCreditsModalRef"
@success="handleTopupSuccess"
/>
<SwitchCurrencyDialog
ref="switchCurrencyDialogRef"
:target-currency="pendingCurrency"
:is-loading="isSwitchingCurrency"
@confirm="onConfirmSwitchCurrency"
/>
</template>
</SettingsLayout>
</template>
@@ -0,0 +1,57 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import { getCurrencyConfig } from 'dashboard/constants/billing';
const props = defineProps({
targetCurrency: {
type: String,
default: '',
},
isLoading: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['confirm']);
const { t } = useI18n();
const dialogRef = ref(null);
const currencyLabel = computed(() =>
t(getCurrencyConfig(props.targetCurrency).i18nLabelKey)
);
const open = () => dialogRef.value?.open();
const close = () => dialogRef.value?.close();
defineExpose({ open, close });
</script>
<template>
<Dialog
ref="dialogRef"
type="alert"
:title="$t('BILLING_SETTINGS.CURRENCY.CONFIRM.TITLE')"
:description="
$t('BILLING_SETTINGS.CURRENCY.CONFIRM.DESCRIPTION', {
currency: currencyLabel,
})
"
:confirm-button-label="
$t('BILLING_SETTINGS.CURRENCY.CONFIRM.CONFIRM_BUTTON')
"
:cancel-button-label="$t('BILLING_SETTINGS.CURRENCY.CONFIRM.CANCEL_BUTTON')"
:is-loading="isLoading"
@confirm="emit('confirm')"
>
<div class="p-2.5 rounded-lg bg-n-amber-2 border border-n-amber-6">
<p class="text-sm text-n-amber-11">
{{ $t('BILLING_SETTINGS.CURRENCY.CONFIRM.WARNING') }}
</p>
</div>
</Dialog>
</template>
@@ -19,6 +19,7 @@ const state = {
isUpdating: false,
isCheckoutInProcess: false,
isFetchingLimits: false,
isSwitchingCurrency: false,
},
};
@@ -142,6 +143,20 @@ export const actions = {
}
},
switchBillingCurrency: async ({ commit, dispatch }, { currency }) => {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isSwitchingCurrency: true });
try {
await EnterpriseAccountAPI.switchCurrency(currency);
// Refresh the account so custom_attributes.billing_currency and the
// subscription details reflect the new currency.
await dispatch('get', { silent: true });
} catch (error) {
throwErrorMessage(error);
} finally {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isSwitchingCurrency: false });
}
},
limits: async ({ commit }) => {
commit(types.default.SET_ACCOUNT_UI_FLAG, { isFetchingLimits: true });
try {