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:
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user