Pricing Upgrade Flow changes (#514)

* Pricing Upgrade Flow changes

* remove extra code

* Refactor subscription plan selection and billing management

* Polish the new pricing modal

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Chirag Chhatrala
2024-08-23 15:53:01 +05:30
committed by GitHub
parent a73badb091
commit fedc382594
16 changed files with 1146 additions and 366 deletions

View File

@@ -185,28 +185,28 @@
class="px-4"
>
<UAlert
class="mt-4"
icon="i-heroicons-command-line"
class="mt-8 p-4"
icon="i-heroicons-sparkles"
color="primary"
variant="subtle"
description="You can add components to your app using the cli."
>
<template #title>
<h2 class="font-medium text-lg -mt-2">
<h3 class="font-semibold text-md">
Discover our Pro plan
</h2>
</h3>
</template>
<template #description>
<div class="flex flex-wrap sm:flex-nowrap gap-2 items-start">
<div class="flex flex-wrap sm:flex-nowrap gap-4 items-start">
<p class="flex-grow">
Remove NoteForms branding, customize forms further, use your custom domain, integrate with your
favorite tools, invite users, and more!
</p>
<UButton
v-track.upgrade_banner_home_click
:to="{name:'pricing'}"
color="white"
class="block"
@click.prevent="subscriptionModalStore.openModal()"
>
Upgrade Now
</UButton>
@@ -246,6 +246,7 @@ useOpnSeoMeta({
"All of your OpnForm are here. Create new forms, or update your existing forms.",
})
const subscriptionModalStore = useSubscriptionModalStore()
const formsStore = useFormsStore()
const workspacesStore = useWorkspacesStore()
formsStore.startLoading()

View File

@@ -21,8 +21,18 @@
:loading="billingLoading"
@click="openBillingDashboard"
>
Manage Subscription
<span v-if="canCancel">Manage Subscription & Invoices</span>
<span else>Billing & Invoices</span>
</UButton>
<br><br>
<v-button
v-if="canCancel"
color="white"
:loading="cancelLoading"
@click.prevent="cancelSubscription"
>
Cancel Subscription
</v-button>
</div>
</template>
@@ -45,6 +55,7 @@ definePageMeta({
const authStore = useAuthStore()
const user = computed(() => authStore.user)
const billingLoading = ref(false)
const cancelLoading = ref(false)
const usersCount = ref(0)
onMounted(() => {
@@ -61,18 +72,60 @@ const loadUsersCount = () => {
})
}
const canCancel = computed(() => {
return user.value.subscriptions.some(sub => (sub.stripe_status === 'active') || (sub.stripe_status === 'trialing' && sub.ends_at == null))
})
const cancelSubscription = () => {
cancelLoading.value = true
opnFetch('/subscription').then((data) => {
if (data && data.length) {
window.profitwell('init_cancellation_flow', { subscription_id: data[0].stripe_id }).then(result => {
// This means the customer either aborted the flow (i.e.
// they clicked on "never mind, I don't want to cancel"), or
// accepted a salvage attempt or salvage offer.
// Thus, do nothing since they won't cancel.
if (result.status === 'retained' || result.status === 'aborted') {
console.log('Retained 🥳')
} else {
opnFetch('/subscription/' + data[0].id + '/cancel', { method: 'POST' })
.then(() => {
useAlert().success('Subscription cancelled. Sorry to see you leave 😢')
})
.catch(() => {
useAlert().error('Sorry to see you leave 😢 We\'re currently having issues with subscriptions. Please ' +
'send us a message via the livechat, and we will cancel your subscription.')
}).finally(() => {
cancelLoading.value = false
useAmplitude().logEvent('subscription_cancelled')
useCrisp().pushEvent('subscription_cancelled')
// Now we need to reload workspace and user
opnFetch('user').then((userData) => {
authStore.setUser(userData)
})
fetchAllWorkspaces().then((workspaces) => {
workspacesStore.set(workspaces.data.value)
})
})
}
})
}
}).finally(() => {
useCrisp().pushEvent('subscription_cancelled')
cancelLoading.value = false
})
}
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
})
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>

View File

@@ -1,34 +1,14 @@
<template />
<script setup>
import { useBroadcastChannel } from '@vueuse/core'
<script>
import { computed } from "vue"
import { useAuthStore } from "../../stores/auth"
definePageMeta({
middleware: 'auth'
})
export default {
components: {},
layout: "default",
middleware: "auth",
const subscribeBroadcast = useBroadcastChannel('subscribe')
setup() {
useOpnSeoMeta({
title: "Error",
})
const authStore = useAuthStore()
return {
authenticated: computed(() => authStore.check),
}
},
data: () => ({}),
computed: {},
mounted() {
this.$router.push({ name: "pricing" })
useAlert().error(
"Unfortunately we could not confirm your subscription. Please try again and contact us if the issue persists.",
)
},
}
</script>
onMounted(() => {
subscribeBroadcast.post({ 'type': 'error' })
window.close()
})
</script>

View File

@@ -17,86 +17,55 @@
</div>
</template>
<script>
import {computed} from "vue"
import {useAuthStore} from "../../stores/auth"
export default {
layout: "default",
middleware: "auth",
<script setup>
import { useBroadcastChannel } from '@vueuse/core'
setup() {
useOpnSeoMeta({
title: "Subscription Success",
})
definePageMeta({
middleware: 'auth'
})
const authStore = useAuthStore()
const confetti = useConfetti()
return {
authStore,
confetti,
authenticated: computed(() => authStore.check),
user: computed(() => authStore.user),
crisp: useCrisp(),
}
},
useOpnSeoMeta({
title: 'Subscription Success'
})
data: () => ({
interval: null,
}),
const authStore = useAuthStore()
const confetti = useConfetti()
const user = computed(() => authStore.user)
const subscribeBroadcast = useBroadcastChannel('subscribe')
computed: {},
const interval = ref(null)
mounted() {
this.redirectIfSubscribed()
this.interval = setInterval(() => this.checkSubscription(), 5000)
},
beforeUnmount() {
clearInterval(this.interval)
},
unmounted() {
// stop confettis after 2 sec
setTimeout(() => {
this.confetti.stop()
}, 2000)
},
methods: {
async checkSubscription() {
// Fetch the user.
await this.authStore.fetchUser()
this.redirectIfSubscribed()
},
redirectIfSubscribed() {
if (this.user.is_subscribed) {
useAmplitude().logEvent("subscribed", {
plan: "pro",
})
this.crisp.pushEvent("subscribed", {
plan: "pro",
})
try {
useGtm().trackEvent({event: 'subscribed'})
} catch (error) {
console.error(error)
}
this.confetti.play()
this.$router.push({name: "home"})
if (this.user.has_enterprise_subscription) {
useAlert().success(
"Awesome! Your subscription to OpnForm is now confirmed! You now have access to all Enterprise " +
"features. No need to invite your teammates, just ask them to create a OpnForm account and to connect the same Notion workspace. Feel free to contact us if you have any question 🙌",
)
} else {
useAlert().success(
"Awesome! Your subscription to OpnForm is now confirmed! You now have access to all Pro " +
"features. Feel free to contact us if you have any question 🙌",
)
}
}
},
const redirectIfSubscribed = () => {
if (user.value.is_subscribed) {
subscribeBroadcast.post({ 'type': 'success' })
window.close()
}
}
</script>
const checkSubscription = () => {
// Fetch the user.
return noteFormsFetch('user').then((data) => {
authStore.setUser(data)
redirectIfSubscribed()
}).catch((error) => {
console.error(error)
clearInterval(interval.value)
})
}
onMounted(() => {
redirectIfSubscribed()
interval.value = setInterval(() => checkSubscription(), 5000)
})
onBeforeUnmount(() => {
clearInterval(interval.value)
})
onUnmounted(() => {
// stop confettis after 2 sec
setTimeout(() => {
confetti.stop()
}, 2000)
})
</script>