feat: verify billing details before checkout
This commit is contained in:
@@ -27,6 +27,18 @@ class EnterpriseAccountAPI extends ApiClient {
|
||||
createTopupCheckout(credits) {
|
||||
return axios.post(`${this.url}topup_checkout`, { credits });
|
||||
}
|
||||
|
||||
getBillingDetails() {
|
||||
return axios.get(`${this.url}billing_details`);
|
||||
}
|
||||
|
||||
confirmBillingDetails({ businessName, businessAddress, billingEmail }) {
|
||||
return axios.post(`${this.url}confirm_billing_details`, {
|
||||
business_name: businessName,
|
||||
business_address: businessAddress,
|
||||
billing_email: billingEmail,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new EnterpriseAccountAPI();
|
||||
|
||||
@@ -483,6 +483,31 @@
|
||||
"GO_BACK": "Go Back",
|
||||
"CONFIRM_PURCHASE": "Confirm Purchase"
|
||||
}
|
||||
},
|
||||
"CONFIRM_BUSINESS": {
|
||||
"TITLE": "Confirm Business Details",
|
||||
"DESCRIPTION": "Please confirm your business information before accessing the billing portal.",
|
||||
"PREFILLED_BANNER": "We found your existing business details. Review and update if needed, or continue to proceed.",
|
||||
"BUSINESS_NAME": {
|
||||
"LABEL": "Business Name",
|
||||
"PLACEHOLDER": "Enter your business name",
|
||||
"REQUIRED": "Business name is required"
|
||||
},
|
||||
"BUSINESS_ADDRESS": {
|
||||
"LABEL": "Business Address",
|
||||
"PLACEHOLDER": "Enter your business address",
|
||||
"REQUIRED": "Business address is required"
|
||||
},
|
||||
"BILLING_EMAIL": {
|
||||
"LABEL": "Billing Email",
|
||||
"PLACEHOLDER": "Enter your billing email",
|
||||
"REQUIRED": "Billing email is required",
|
||||
"INVALID": "Please enter a valid email address",
|
||||
"HELP_TEXT": "All billing-related emails will be sent to this address"
|
||||
},
|
||||
"CANCEL": "Cancel",
|
||||
"CONTINUE": "Continue to Billing Portal",
|
||||
"ERROR": "Failed to update business details. Please try again."
|
||||
}
|
||||
},
|
||||
"SECURITY_SETTINGS": {
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 ConfirmBusinessDetailsModal from './components/ConfirmBusinessDetailsModal.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import ButtonV4 from 'next/button/Button.vue';
|
||||
@@ -35,11 +36,16 @@ const BILLING_REFRESH_ATTEMPTED = 'billing_refresh_attempted';
|
||||
// State for handling refresh attempts and loading
|
||||
const isWaitingForBilling = ref(false);
|
||||
const purchaseCreditsModalRef = ref(null);
|
||||
const confirmBusinessDetailsModalRef = ref(null);
|
||||
|
||||
const customAttributes = computed(() => {
|
||||
return currentAccount.value.custom_attributes || {};
|
||||
});
|
||||
|
||||
const billingDetailsConfirmed = computed(() => {
|
||||
return customAttributes.value.billing_details_confirmed;
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed property for plan name
|
||||
* @returns {string|undefined}
|
||||
@@ -119,7 +125,15 @@ const handleBillingPageLogic = async () => {
|
||||
};
|
||||
|
||||
const onClickBillingPortal = () => {
|
||||
store.dispatch('accounts/checkout');
|
||||
if (billingDetailsConfirmed.value) {
|
||||
store.dispatch('accounts/checkout');
|
||||
} else {
|
||||
confirmBusinessDetailsModalRef.value?.open();
|
||||
}
|
||||
};
|
||||
|
||||
const onBusinessDetailsConfirmed = redirectUrl => {
|
||||
window.location = redirectUrl;
|
||||
};
|
||||
|
||||
const onToggleChatWindow = () => {
|
||||
@@ -263,6 +277,10 @@ onMounted(handleBillingPageLogic);
|
||||
ref="purchaseCreditsModalRef"
|
||||
@success="handleTopupSuccess"
|
||||
/>
|
||||
<ConfirmBusinessDetailsModal
|
||||
ref="confirmBusinessDetailsModalRef"
|
||||
@confirmed="onBusinessDetailsConfirmed"
|
||||
/>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
<script setup>
|
||||
import { ref, computed, reactive } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, email } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||
import EnterpriseAccountAPI from 'dashboard/api/enterprise/account';
|
||||
|
||||
const emit = defineEmits(['confirmed']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
const isLoading = ref(false);
|
||||
const isFetchingDetails = ref(false);
|
||||
const hasPreExistingData = ref(false);
|
||||
|
||||
const formValues = reactive({
|
||||
businessName: '',
|
||||
businessAddress: '',
|
||||
billingEmail: '',
|
||||
});
|
||||
|
||||
const validationRules = computed(() => ({
|
||||
businessName: { required },
|
||||
businessAddress: { required },
|
||||
billingEmail: { required, email },
|
||||
}));
|
||||
|
||||
const v$ = useVuelidate(validationRules, formValues);
|
||||
|
||||
const fieldKeyMap = {
|
||||
businessName: 'BUSINESS_NAME',
|
||||
businessAddress: 'BUSINESS_ADDRESS',
|
||||
billingEmail: 'BILLING_EMAIL',
|
||||
};
|
||||
|
||||
const getErrorMessage = field => {
|
||||
const fieldState = v$.value[field];
|
||||
if (!fieldState || !fieldState.$error) return '';
|
||||
|
||||
if (fieldState.required?.$invalid) {
|
||||
return t(
|
||||
`BILLING_SETTINGS.CONFIRM_BUSINESS.${fieldKeyMap[field]}.REQUIRED`
|
||||
);
|
||||
}
|
||||
if (fieldState.email?.$invalid) {
|
||||
return t('BILLING_SETTINGS.CONFIRM_BUSINESS.BILLING_EMAIL.INVALID');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const fetchBillingDetails = async () => {
|
||||
isFetchingDetails.value = true;
|
||||
try {
|
||||
const { data } = await EnterpriseAccountAPI.getBillingDetails();
|
||||
if (data.business_name || data.business_address || data.billing_email) {
|
||||
formValues.businessName = data.business_name || '';
|
||||
formValues.businessAddress = data.business_address || '';
|
||||
formValues.billingEmail = data.billing_email || '';
|
||||
hasPreExistingData.value = true;
|
||||
}
|
||||
} catch {
|
||||
// Silently fail - user can still fill in details manually
|
||||
} finally {
|
||||
isFetchingDetails.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
v$.value.$reset();
|
||||
formValues.businessName = '';
|
||||
formValues.businessAddress = '';
|
||||
formValues.billingEmail = '';
|
||||
hasPreExistingData.value = false;
|
||||
dialogRef.value?.open();
|
||||
fetchBillingDetails();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
dialogRef.value?.close();
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const { data } = await EnterpriseAccountAPI.confirmBillingDetails({
|
||||
businessName: formValues.businessName,
|
||||
businessAddress: formValues.businessAddress,
|
||||
billingEmail: formValues.billingEmail,
|
||||
});
|
||||
emit('confirmed', data.redirect_url);
|
||||
close();
|
||||
} catch {
|
||||
useAlert(t('BILLING_SETTINGS.CONFIRM_BUSINESS.ERROR'));
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ open, close });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
:title="$t('BILLING_SETTINGS.CONFIRM_BUSINESS.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS.CONFIRM_BUSINESS.DESCRIPTION')"
|
||||
:show-confirm-button="false"
|
||||
:show-cancel-button="false"
|
||||
width="lg"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div
|
||||
v-if="hasPreExistingData"
|
||||
class="flex items-center gap-2 p-3 rounded-lg bg-n-teal-2 border border-n-teal-6"
|
||||
>
|
||||
<fluent-icon icon="checkmark-circle" size="16" class="text-n-teal-11" />
|
||||
<p class="text-sm text-n-teal-11">
|
||||
{{ $t('BILLING_SETTINGS.CONFIRM_BUSINESS.PREFILLED_BANNER') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 text-heading-3 text-n-slate-12">
|
||||
{{ $t('BILLING_SETTINGS.CONFIRM_BUSINESS.BUSINESS_NAME.LABEL') }}
|
||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span class="text-n-ruby-9">*</span>
|
||||
</label>
|
||||
<Input
|
||||
v-model="formValues.businessName"
|
||||
:placeholder="
|
||||
$t('BILLING_SETTINGS.CONFIRM_BUSINESS.BUSINESS_NAME.PLACEHOLDER')
|
||||
"
|
||||
:message="getErrorMessage('businessName')"
|
||||
:message-type="v$.businessName.$error ? 'error' : 'info'"
|
||||
@blur="v$.businessName.$touch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 text-heading-3 text-n-slate-12">
|
||||
{{ $t('BILLING_SETTINGS.CONFIRM_BUSINESS.BUSINESS_ADDRESS.LABEL') }}
|
||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span class="text-n-ruby-9">*</span>
|
||||
</label>
|
||||
<TextArea
|
||||
v-model="formValues.businessAddress"
|
||||
:placeholder="
|
||||
$t('BILLING_SETTINGS.CONFIRM_BUSINESS.BUSINESS_ADDRESS.PLACEHOLDER')
|
||||
"
|
||||
:message="getErrorMessage('businessAddress')"
|
||||
:message-type="v$.businessAddress.$error ? 'error' : 'info'"
|
||||
:max-length="500"
|
||||
min-height="5rem"
|
||||
max-height="8rem"
|
||||
@blur="v$.businessAddress.$touch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 text-heading-3 text-n-slate-12">
|
||||
{{ $t('BILLING_SETTINGS.CONFIRM_BUSINESS.BILLING_EMAIL.LABEL') }}
|
||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span class="text-n-ruby-9">*</span>
|
||||
</label>
|
||||
<Input
|
||||
v-model="formValues.billingEmail"
|
||||
type="email"
|
||||
:placeholder="
|
||||
$t('BILLING_SETTINGS.CONFIRM_BUSINESS.BILLING_EMAIL.PLACEHOLDER')
|
||||
"
|
||||
:message="getErrorMessage('billingEmail')"
|
||||
:message-type="v$.billingEmail.$error ? 'error' : 'info'"
|
||||
@blur="v$.billingEmail.$touch"
|
||||
/>
|
||||
<p v-if="!v$.billingEmail.$error" class="mt-1 text-xs text-n-slate-10">
|
||||
{{ $t('BILLING_SETTINGS.CONFIRM_BUSINESS.BILLING_EMAIL.HELP_TEXT') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between w-full gap-3">
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="$t('BILLING_SETTINGS.CONFIRM_BUSINESS.CANCEL')"
|
||||
class="w-full"
|
||||
:disabled="isLoading"
|
||||
@click="close"
|
||||
/>
|
||||
<Button
|
||||
color="blue"
|
||||
:label="$t('BILLING_SETTINGS.CONFIRM_BUSINESS.CONTINUE')"
|
||||
class="w-full"
|
||||
:is-loading="isLoading"
|
||||
@click="handleConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user