feat(billing): let accounts switch billing currency with a confirmation note
This commit is contained in:
@@ -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>
|
||||
|
||||
+57
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user