Billing flow redirect (#713)

* Billing flow redirect

* fix checkout url

* Refactor checkout and subscription flow

- Improve checkout URL generation with ref unwrapping
- Enhance user data handling in subscription modal
- Remove deprecated CheckoutDetailsModal component
- Update form field and register form styling
- Add more robust user data update mechanism

* Refactor checkout and subscription flow

- Improve checkout URL generation with ref unwrapping
- Enhance user data handling in subscription modal
- Remove deprecated CheckoutDetailsModal component
- Update form field and register form styling
- Add more robust user data update mechanism

* Fix accessibility and checkout URL generation

- Add proper label for terms and conditions checkbox in RegisterForm
- Refactor checkout URL generation in SubscriptionModal using computed refs
- Improve form input handling and reactivity

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Chirag Chhatrala
2025-03-05 16:46:33 +05:30
committed by GitHub
parent b633f97ce1
commit a59b46665c
9 changed files with 193 additions and 221 deletions

View File

@@ -69,10 +69,11 @@
<checkbox-input
:form="form"
name="agree_terms"
class="mb-3"
class="my-3"
:required="true"
>
<template #label>
<label for="agree_terms">
I agree with the
<NuxtLink
:to="{ name: 'terms-conditions' }"
@@ -90,6 +91,7 @@
Privacy policy
</NuxtLink>
of the website and I accept them.
</label>
</template>
</checkbox-input>

View File

@@ -1,130 +0,0 @@
<template>
<modal
:show="show"
max-width="lg"
@close="close"
>
<text-input
ref="companyName"
label="Company Name"
name="name"
:required="true"
:form="form"
help="Name that will appear on invoices"
/>
<text-input
label="Email"
name="email"
native-type="email"
:required="true"
:form="form"
help="Where invoices will be sent"
/>
<v-button
:loading="form.busy || loading"
:disabled="form.busy || loading ? true : null"
class="mt-6 block mx-auto"
:arrow="true"
@click="saveDetails"
>
Go to checkout
</v-button>
</modal>
</template>
<script>
import { computed } from "vue"
import TextInput from "../../forms/TextInput.vue"
import VButton from "~/components/global/VButton.vue"
export default {
components: { VButton, TextInput },
props: {
show: {
type: Boolean,
default: false,
},
plan: {
type: String,
default: "pro",
},
yearly: {
type: Boolean,
default: true,
},
},
emits: ['close'],
setup() {
const authStore = useAuthStore()
return {
user: computed(() => authStore.user),
}
},
data: () => ({
form: useForm({
name: "",
email: "",
}),
loading: false,
}),
computed: {},
watch: {
user() {
this.updateUser()
},
show() {
// Wait for modal to open and focus on first field
setTimeout(() => {
if (this.$refs.companyName) {
this.$refs.companyName.$el.querySelector("input").focus()
}
}, 300)
this.loading = false
},
},
mounted() {
this.updateUser()
},
methods: {
updateUser() {
if (this.user) {
this.form.name = this.user.name
this.form.email = this.user.email
}
},
saveDetails() {
if (this.form.busy) return
this.form.put("subscription/update-customer-details").then(() => {
this.loading = true
opnFetch(
"/subscription/new/" +
this.plan +
"/" +
(!this.yearly ? "monthly" : "yearly") +
"/checkout/with-trial",
)
.then((data) => {
window.location = data.checkout_url
})
.catch((error) => {
useAlert().error(error.data.message)
})
.finally(() => {
this.loading = false
this.close()
})
})
},
close() {
this.$emit("close")
},
},
}
</script>

View File

@@ -141,7 +141,8 @@
v-else-if="authenticated && user.is_subscribed"
class="mr-1"
:arrow="true"
@click.prevent="openBilling"
:to="{ name: 'redirect-billing-portal' }"
target="_blank"
>
View Billing
</v-button>
@@ -222,15 +223,6 @@ export default {
computed: {},
methods: {
openBilling() {
this.billingLoading = true
opnFetch("/subscription/billing-portal").then((data) => {
this.billingLoading = false
const url = data.portal_url
window.location = url
})
},
},
methods: {},
}
</script>

View File

@@ -92,9 +92,9 @@
<v-button
v-else
:loading="billingLoading"
class="relative border border-white border-opacity-20 h-10 inline-flex px-4 items-center rounded-lg text-sm font-semibold w-full justify-center mt-4"
:to="{ name: 'redirect-billing-portal' }"
target="_blank"
@click="openBillingDashboard"
class="relative border border-white border-opacity-20 h-10 inline-flex px-4 items-center rounded-lg text-sm font-semibold w-full justify-center mt-4"
>
Manage Plan
</v-button>
@@ -301,7 +301,9 @@
size="md"
class="w-auto flex-grow"
:loading="form.busy || loading"
@click="saveDetails"
:disabled="form.busy || loading"
:to="checkoutUrl"
target="_blank"
>
<template v-if="isSubscribed">
Upgrade
@@ -329,6 +331,7 @@
<script setup>
import SlidingTransition from '~/components/global/transitions/SlidingTransition.vue'
import { fetchAllWorkspaces } from '~/stores/workspaces.js'
import { useCheckoutUrl } from '@/composables/useCheckoutUrl'
const router = useRouter()
const subscriptionModalStore = useSubscriptionModalStore()
@@ -354,17 +357,31 @@ const user = computed(() => authStore.user)
const isSubscribed = computed(() => workspacesStore.isSubscribed)
const currency = 'usd'
const checkoutUrl = useCheckoutUrl(
computed(() => form.name),
computed(() => form.email),
currentPlan,
isYearly,
currency
)
// When opening modal with a plan already (and user not subscribed yet) - skip first step
watch(() => subscriptionModalStore.show, () => {
currentStep.value = 1
if (subscriptionModalStore.show && subscriptionModalStore.plan) {
if (user.value.is_subscribed) {
return
// Update user data when modal opens
if (subscriptionModalStore.show) {
updateUser()
if (subscriptionModalStore.plan) {
if (user.value.is_subscribed) {
return
}
isYearly.value = subscriptionModalStore.yearly
shouldShowUpsell.value = !isYearly.value
currentStep.value = 2
currentPlan.value = subscriptionModalStore.plan
}
isYearly.value = subscriptionModalStore.yearly
shouldShowUpsell.value = !isYearly.value
currentStep.value = 2
currentPlan.value = subscriptionModalStore.plan
}
})
@@ -421,12 +438,29 @@ watch(broadcastData, () => {
})
onMounted(() => {
if (user && user.value) {
form.name = user.value.name
form.email = user.value.email
}
updateUser()
})
// Update form with user data - sets company name to user name by default
const updateUser = () => {
if (user.value) {
// Set company name to user name by default
if (user.value.name && !form.name) {
form.name = user.value.name
}
// Set email if available
if (user.value.email && !form.email) {
form.email = user.value.email
}
}
}
// Watch for user changes
watch(user, () => {
updateUser()
}, { immediate: true })
const onSelectPlan = (planName) => {
if (!authenticated.value) {
subscriptionModalStore.closeModal()
@@ -443,56 +477,4 @@ const onSelectPlan = (planName) => {
const goBackToStep1 = () => {
currentStep.value = 1
}
const saveDetails = () => {
if (form.busy)
return
form.put('subscription/update-customer-details').then(() => {
loading.value = true
// Get param trial_duration from url
const urlParams = new URLSearchParams(window.location.search)
const trialDuration = urlParams.get('trial_duration') || null
if (trialDuration) {
useAmplitude().logEvent('extended_trial_used', {
duration: trialDuration
})
}
const params = {
trial_duration: trialDuration,
currency: currency
}
opnFetch(
`/subscription/new/${
currentPlan.value
}/${
!isYearly.value ? 'monthly' : 'yearly'
}/checkout/with-trial?${
new URLSearchParams(params).toString()}`
)
.then((data) => {
window.open(data.checkout_url, '_blank')
})
.catch((error) => {
useAlert().error(error.data.message)
loading.value = false
})
.finally(() => {
})
})
}
const openBillingDashboard = () => {
billingLoading.value = true
opnFetch('/subscription/billing-portal').then((data) => {
const url = data.portal_url
window.location = url
}).catch((error) => {
useAlert().error(error.data.message)
}).finally(() => {
billingLoading.value = false
})
}
</script>