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:
parent
a73badb091
commit
fedc382594
|
|
@ -49,6 +49,7 @@
|
||||||
|
|
||||||
<NotificationsWrapper />
|
<NotificationsWrapper />
|
||||||
<feature-base />
|
<feature-base />
|
||||||
|
<SubscriptionModal />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,18 @@
|
||||||
<div
|
<div
|
||||||
v-if="show"
|
v-if="show"
|
||||||
ref="backdrop"
|
ref="backdrop"
|
||||||
class="fixed z-30 top-0 inset-0 px-4 sm:px-0 flex items-top justify-center bg-gray-700/75 w-full h-screen overflow-y-scroll"
|
class="fixed z-30 top-0 inset-0 px-2 sm:px-4 flex items-top justify-center bg-gray-700/75 w-full h-screen overflow-y-scroll"
|
||||||
:class="{ 'backdrop-blur-sm': backdropBlur }"
|
:class="{ 'backdrop-blur-sm': backdropBlur }"
|
||||||
@click.self="close"
|
@click.self="close"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="content"
|
ref="content"
|
||||||
class="self-start bg-white dark:bg-notion-dark w-full relative my-6 rounded-xl shadow-xl"
|
class="self-start bg-white dark:bg-notion-dark w-full relative my-2 sm:my-6 rounded-xl shadow-xl"
|
||||||
:class="maxWidthClass"
|
:class="maxWidthClass"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="closeable"
|
v-if="closeable"
|
||||||
class="absolute top-4 right-4 z-10"
|
class="absolute top-4 right-4"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="text-gray-500 hover:text-gray-900 cursor-pointer"
|
class="text-gray-500 hover:text-gray-900 cursor-pointer"
|
||||||
|
|
@ -93,84 +93,85 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { watch } from "vue"
|
import { watch } from 'vue'
|
||||||
import { default as _has } from "lodash/has"
|
import { default as _has } from 'lodash/has'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false
|
||||||
},
|
},
|
||||||
backdropBlur: {
|
backdropBlur: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false
|
||||||
},
|
},
|
||||||
iconColor: {
|
iconColor: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "blue",
|
default: 'blue'
|
||||||
},
|
},
|
||||||
maxWidth: {
|
maxWidth: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "2xl",
|
default: '2xl'
|
||||||
},
|
},
|
||||||
innerPadding: {
|
innerPadding: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "p-6",
|
default: 'p-6'
|
||||||
},
|
},
|
||||||
headerInnerPadding: {
|
headerInnerPadding: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "p-6",
|
default: 'p-6'
|
||||||
},
|
},
|
||||||
footerInnerPadding: {
|
footerInnerPadding: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "p-6",
|
default: 'p-6'
|
||||||
},
|
},
|
||||||
closeable: {
|
closeable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true
|
||||||
},
|
},
|
||||||
compactHeader: {
|
compactHeader: {
|
||||||
default: false,
|
default: false,
|
||||||
type: Boolean,
|
type: Boolean
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(["close"])
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
bodyAttrs: computed(() => {
|
bodyAttrs: computed(() => {
|
||||||
return {
|
return {
|
||||||
class: {
|
class: {
|
||||||
"overflow-hidden": props.show,
|
'overflow-hidden': props.show
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const closeOnEscape = (e) => {
|
const closeOnEscape = (e) => {
|
||||||
if (e.key === "Escape" && props.show) {
|
if (e.key === 'Escape' && props.show) {
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (import.meta.server) return
|
if (import.meta.server) return
|
||||||
document.addEventListener("keydown", closeOnEscape)
|
document.addEventListener('keydown', closeOnEscape)
|
||||||
initMotions()
|
initMotions()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (import.meta.server) return
|
if (import.meta.server) return
|
||||||
document.removeEventListener("keydown", closeOnEscape)
|
document.removeEventListener('keydown', closeOnEscape)
|
||||||
})
|
})
|
||||||
|
|
||||||
const maxWidthClass = computed(() => {
|
const maxWidthClass = computed(() => {
|
||||||
return {
|
return {
|
||||||
sm: "sm:max-w-sm",
|
sm: 'sm:max-w-sm',
|
||||||
md: "sm:max-w-md",
|
md: 'sm:max-w-md',
|
||||||
lg: "sm:max-w-lg",
|
lg: 'sm:max-w-lg',
|
||||||
xl: "sm:max-w-xl",
|
xl: 'sm:max-w-xl',
|
||||||
"2xl": "sm:max-w-2xl",
|
'2xl': 'max-w-2xl',
|
||||||
|
'screen-lg': 'max-w-screen-lg'
|
||||||
}[props.maxWidth]
|
}[props.maxWidth]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -180,15 +181,15 @@ const motionFadeIn = {
|
||||||
transition: {
|
transition: {
|
||||||
delay: 100,
|
delay: 100,
|
||||||
duration: 200,
|
duration: 200,
|
||||||
ease: "easeIn",
|
ease: 'easeIn'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
enter: {
|
enter: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 200,
|
duration: 200
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const motionSlideBottom = {
|
const motionSlideBottom = {
|
||||||
|
|
@ -196,19 +197,19 @@ const motionSlideBottom = {
|
||||||
y: 150,
|
y: 150,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
transition: {
|
transition: {
|
||||||
ease: "easeIn",
|
ease: 'easeIn',
|
||||||
duration: 200,
|
duration: 200
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
enter: {
|
enter: {
|
||||||
y: 0,
|
y: 0,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 250,
|
duration: 250,
|
||||||
ease: "easeOut",
|
ease: 'easeOut',
|
||||||
delay: 100,
|
delay: 100
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onLeave = (el, done) => {
|
const onLeave = (el, done) => {
|
||||||
|
|
@ -218,7 +219,7 @@ const onLeave = (el, done) => {
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
if (props.closeable) {
|
if (props.closeable) {
|
||||||
emit("close")
|
emit('close')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<UTooltip
|
||||||
v-if="shouldDisplayProTag"
|
v-if="shouldDisplayProTag"
|
||||||
class="inline"
|
:text="upgradeModalTitle??'You need a Pro plan to use this feature'"
|
||||||
|
class="inline normal-case"
|
||||||
>
|
>
|
||||||
<UTooltip text="Upgrade to use this feature">
|
<div
|
||||||
<div
|
v-track.pro_tag_click="{title:upgradeModalTitle}"
|
||||||
role="button"
|
class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold cursor-pointer"
|
||||||
class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold cursor-pointer"
|
@click.stop="onClick"
|
||||||
@click="showPremiumModal = true"
|
>
|
||||||
>
|
PRO
|
||||||
PRO
|
</div>
|
||||||
</div>
|
</UTooltip>
|
||||||
<modal
|
|
||||||
:show="showPremiumModal"
|
|
||||||
@close="showPremiumModal = false"
|
|
||||||
>
|
|
||||||
<h2 class="text-nt-blue">
|
|
||||||
OpnForm PRO
|
|
||||||
</h2>
|
|
||||||
<h4
|
|
||||||
v-if="user && user.is_subscribed"
|
|
||||||
class="text-center mt-5"
|
|
||||||
>
|
|
||||||
We're happy to have you as a Pro customer. If you're having any issue
|
|
||||||
with OpnForm, or if you have a feature request, please
|
|
||||||
<a href="mailto:contact@opnform.com">contact us</a>.
|
|
||||||
</h4>
|
|
||||||
<div
|
|
||||||
v-if="!user || !user.is_subscribed"
|
|
||||||
class="mt-4"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
All the features with a<span
|
|
||||||
class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold mx-1"
|
|
||||||
>
|
|
||||||
PRO
|
|
||||||
</span>
|
|
||||||
tag are available in the Pro plan of OpnForm.
|
|
||||||
<b>You can play around and try all Pro features within the form
|
|
||||||
editor, but you can't use them in your real forms</b>. You can subscribe now to gain unlimited access to all our pro
|
|
||||||
features!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-4 text-center">
|
|
||||||
<v-button
|
|
||||||
color="white"
|
|
||||||
@click="showPremiumModal = false"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</v-button>
|
|
||||||
</div>
|
|
||||||
</modal>
|
|
||||||
</UTooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
upgradeModalTitle: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
upgradeModalDescription: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const subscriptionModalStore = useSubscriptionModalStore()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const workspacesStore = useWorkspacesStore()
|
const workspacesStore = useWorkspacesStore()
|
||||||
const user = computed(() => authStore.user)
|
const user = computed(() => authStore.user)
|
||||||
const workspace = computed(() => workspacesStore.getCurrent)
|
const workspace = computed(() => workspacesStore.getCurrent)
|
||||||
const showPremiumModal = ref(false)
|
|
||||||
|
|
||||||
const shouldDisplayProTag = computed(() => {
|
const shouldDisplayProTag = computed(() => {
|
||||||
if (!useRuntimeConfig().public.paidPlansEnabled) return false
|
if (!useRuntimeConfig().public.paidPlansEnabled) return false
|
||||||
if (!user.value || !workspace.value) return true
|
if (!user.value || !workspace.value) return true
|
||||||
return !workspace.value.is_pro
|
return !workspace.value.is_pro
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function onClick () {
|
||||||
|
subscriptionModalStore.setModalContent(props.upgradeModalTitle, props.upgradeModalDescription)
|
||||||
|
subscriptionModalStore.openModal()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,10 @@
|
||||||
<div class="absolute inset-0 z-10">
|
<div class="absolute inset-0 z-10">
|
||||||
<div class="p-5 max-w-md mx-auto mt-5">
|
<div class="p-5 max-w-md mx-auto mt-5">
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
You need a <pro-tag class="mx-1" /> subscription to access your form
|
You need a <pro-tag
|
||||||
|
upgrade-modal-title="Upgrade today to access form analytics"
|
||||||
|
class="mx-1"
|
||||||
|
/> subscription to access your form
|
||||||
analytics.
|
analytics.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-5 text-center">
|
<p class="mt-5 text-center">
|
||||||
|
|
|
||||||
|
|
@ -149,12 +149,16 @@
|
||||||
<toggle-switch-input
|
<toggle-switch-input
|
||||||
name="no_branding"
|
name="no_branding"
|
||||||
:form="form"
|
:form="form"
|
||||||
|
@update:model-value="onChangeNoBranding"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
Remove OpnForm Branding
|
Remove OpnForm Branding
|
||||||
</span>
|
</span>
|
||||||
<pro-tag class="-mt-1" />
|
<pro-tag
|
||||||
|
upgrade-modal-title="Upgrade today to remove OpnForm branding"
|
||||||
|
class="-mt-1"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</toggle-switch-input>
|
</toggle-switch-input>
|
||||||
<toggle-switch-input
|
<toggle-switch-input
|
||||||
|
|
@ -200,22 +204,44 @@ import GoogleFontPicker from "../../../editors/GoogleFontPicker.vue"
|
||||||
import ProTag from "~/components/global/ProTag.vue"
|
import ProTag from "~/components/global/ProTag.vue"
|
||||||
|
|
||||||
const workingFormStore = useWorkingFormStore()
|
const workingFormStore = useWorkingFormStore()
|
||||||
|
const subscriptionModalStore = useSubscriptionModalStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const workspacesStore = useWorkspacesStore()
|
||||||
const form = storeToRefs(workingFormStore).content
|
const form = storeToRefs(workingFormStore).content
|
||||||
const isMounted = ref(false)
|
const isMounted = ref(false)
|
||||||
const confetti = useConfetti()
|
const confetti = useConfetti()
|
||||||
const showGoogleFontPicker = ref(false)
|
const showGoogleFontPicker = ref(false)
|
||||||
|
|
||||||
|
const user = computed(() => authStore.user)
|
||||||
|
const workspace = computed(() => workspacesStore.getCurrent)
|
||||||
|
|
||||||
|
const isPro = computed(() => {
|
||||||
|
if (!useRuntimeConfig().public.paidPlansEnabled) return true
|
||||||
|
if (!user.value || !workspace.value) return false
|
||||||
|
return workspace.value.is_pro
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
isMounted.value = true
|
isMounted.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
const onChangeConfettiOnSubmission = (val) => {
|
const onChangeConfettiOnSubmission = (val) => {
|
||||||
form.confetti_on_submission = val
|
|
||||||
if (isMounted.value && val) {
|
if (isMounted.value && val) {
|
||||||
confetti.play()
|
confetti.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onChangeNoBranding = (val) => {
|
||||||
|
if (!isPro.value && val) {
|
||||||
|
subscriptionModalStore.setModalContent("Upgrade today to remove OpnForm branding")
|
||||||
|
subscriptionModalStore.openModal()
|
||||||
|
setTimeout(() => {
|
||||||
|
form.value.no_branding = false
|
||||||
|
console.log("form.value.no_branding", form.value.no_branding)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onApplyFont = (val) => {
|
const onApplyFont = (val) => {
|
||||||
form.value.font_family = val
|
form.value.font_family = val
|
||||||
showGoogleFontPicker.value = false
|
showGoogleFontPicker.value = false
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
role="button"
|
role="button"
|
||||||
:class="{
|
:class="{
|
||||||
'hover:bg-blue-50 group cursor-pointer': !unavailable,
|
'hover:bg-blue-50 group cursor-pointer': !unavailable,
|
||||||
'cursor-not-allowed': unavailable,
|
'cursor-not-allowed': integration.coming_soon,
|
||||||
}"
|
}"
|
||||||
class="bg-gray-50 border border-gray-200 rounded-md transition-colors p-4 pb-2 items-center justify-center w-[170px] h-[110px] flex flex-col relative"
|
class="bg-gray-50 border border-gray-200 rounded-md transition-colors p-4 pb-2 items-center justify-center w-[170px] h-[110px] flex flex-col relative"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
|
|
@ -49,8 +49,9 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useWorkspacesStore } from '@/stores/workspaces'
|
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||||
|
|
||||||
const emit = defineEmits(["select"])
|
const emit = defineEmits(["select"])
|
||||||
|
const subscriptionModalStore = useSubscriptionModalStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
integration: {
|
integration: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
@ -76,7 +77,15 @@ const tooltipText = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
if (unavailable.value) return
|
if (props.integration.coming_soon) return
|
||||||
|
if (props.integration.requires_subscription && !currentWorkspace.value.is_pro ) {
|
||||||
|
subscriptionModalStore.setModalContent(
|
||||||
|
'Upgrade today to use this integration',
|
||||||
|
`Upgrade your account to use "${props.integration.name}" and unlock all of our Pro features.`
|
||||||
|
)
|
||||||
|
subscriptionModalStore.openModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
emit("select", props.integration.id)
|
emit("select", props.integration.id)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1,154 +1,113 @@
|
||||||
<template>
|
<template>
|
||||||
<v-transition>
|
<VTransition>
|
||||||
<div
|
<section
|
||||||
v-if="hasCleanings && !hideWarning"
|
v-if="hasCleanings && !hideWarning"
|
||||||
class="border border-gray-300 dark:border-gray-600 rounded-md bg-white p-2"
|
class="flex gap-3 p-4 bg-blue-50 rounded-lg border border-blue-300 border-solid max-md:flex-wrap mb-2"
|
||||||
:class="{
|
aria-labelledby="notification-title"
|
||||||
'hover:bg-yellow-50 dark:hover:bg-yellow-900': !collapseOpened,
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<collapse v-model="collapseOpened">
|
<div class="flex justify-center items-center self-start py-px">
|
||||||
<template #title>
|
<Icon
|
||||||
<p
|
name="bi:stars"
|
||||||
class="text-yellow-500 dark:text-yellow-400 font-semibold text-sm p-1 pr-4"
|
class="w-6 h-6 text-nt-blue"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-1 max-md:max-w-full">
|
||||||
|
<div class="flex flex-col text-sm leading-5 text-slate-900 max-md:max-w-full">
|
||||||
|
<h5
|
||||||
|
id="notification-title"
|
||||||
|
class="font-medium max-md:max-w-full text-blue-500"
|
||||||
>
|
>
|
||||||
<svg
|
Upgrade to unlock all features
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
</h5>
|
||||||
fill="none"
|
<p class="mt-2 max-md:max-w-full text-slate-500">
|
||||||
viewBox="0 0 24 24"
|
<span v-if="specifyFormOwner">Only you are seeing this notification, as owner of the form.</span> The
|
||||||
stroke-width="1.5"
|
following features are disabled on the published form:
|
||||||
stroke="currentColor"
|
|
||||||
class="w-6 h-6 inline"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Some features that are included in our
|
|
||||||
{{ form.is_pro ? "Enterprise" : "Pro" }} plan are disabled when
|
|
||||||
publicly sharing this form<span v-if="specifyFormOwner">
|
|
||||||
(only owners of this form can see this)</span>.
|
|
||||||
</p>
|
</p>
|
||||||
</template>
|
<div
|
||||||
|
class="text-slate-500"
|
||||||
<div class="border-t mt-1 p-4 pb-2 -mx-2">
|
|
||||||
<p
|
|
||||||
class="text-gray-500 text-sm"
|
|
||||||
v-html="cleaningContent"
|
v-html="cleaningContent"
|
||||||
/>
|
/>
|
||||||
<p class="text-gray-500 text-sm mb-4 font-semibold">
|
|
||||||
<NuxtLink :to="{ name: 'pricing' }">
|
|
||||||
{{
|
|
||||||
form.is_pro
|
|
||||||
? "Upgrade your OpnForms plan today"
|
|
||||||
: "Start your free OpnForms trial"
|
|
||||||
}}
|
|
||||||
</NuxtLink>
|
|
||||||
to unlock all of our features and build powerful forms.
|
|
||||||
</p>
|
|
||||||
<div class="flex flex-wrap items-end w-full">
|
|
||||||
<div class="flex-grow flex pr-2">
|
|
||||||
<v-button
|
|
||||||
v-track.upgrade_from_form_cleanings_click
|
|
||||||
size="small"
|
|
||||||
class="inline-block"
|
|
||||||
:to="{ name: 'pricing' }"
|
|
||||||
>
|
|
||||||
{{ form.is_pro ? "Upgrade plan" : "Start free trial" }}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-4 h-4 inline -mt-[2px]"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</v-button>
|
|
||||||
<v-button
|
|
||||||
color="white"
|
|
||||||
size="small"
|
|
||||||
class="ml-2"
|
|
||||||
@click.prevent="openCrisp"
|
|
||||||
>
|
|
||||||
Contact us
|
|
||||||
</v-button>
|
|
||||||
</div>
|
|
||||||
<v-button
|
|
||||||
v-if="hideable"
|
|
||||||
color="white"
|
|
||||||
size="small"
|
|
||||||
class="mt-2"
|
|
||||||
@click.prevent="hideWarning = true"
|
|
||||||
>
|
|
||||||
Hide warning
|
|
||||||
</v-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</collapse>
|
<div class="flex gap-0 self-start mt-2 text-xs font-medium leading-3 text-center">
|
||||||
</div>
|
<UButton
|
||||||
</v-transition>
|
v-track.form_cleanings_unlock_all_features
|
||||||
|
icon="i-heroicons-check-badge-16-solid"
|
||||||
|
@click.prevent="onUpgradeClick"
|
||||||
|
>
|
||||||
|
<template v-if="form.is_pro">
|
||||||
|
Upgrade plan - Unlock all features
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
Start free trial - Unlock all features
|
||||||
|
</template>
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
v-if="specifyFormOwner"
|
||||||
|
variant="link"
|
||||||
|
@click.prevent="hideWarning=true"
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</VTransition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Collapse from "~/components/global/Collapse.vue"
|
import VTransition from '~/components/global/transitions/VTransition.vue'
|
||||||
import VButton from "~/components/global/VButton.vue"
|
|
||||||
import VTransition from "~/components/global/transitions/VTransition.vue"
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "FormCleanings",
|
name: 'FormCleanings',
|
||||||
components: { VTransition, VButton, Collapse },
|
components: { VTransition },
|
||||||
props: {
|
props: {
|
||||||
form: { type: Object, required: true },
|
form: { type: Object, required: true },
|
||||||
specifyFormOwner: { type: Boolean, default: false },
|
specifyFormOwner: { type: Boolean, default: false },
|
||||||
hideable: { type: Boolean, default: false },
|
hideable: { type: Boolean, default: false }
|
||||||
},
|
},
|
||||||
data() {
|
setup () {
|
||||||
|
const subscriptionModalStore = useSubscriptionModalStore()
|
||||||
|
return {
|
||||||
|
subscriptionModalStore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
return {
|
return {
|
||||||
collapseOpened: false,
|
collapseOpened: false,
|
||||||
hideWarning: false,
|
hideWarning: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasCleanings() {
|
hasCleanings () {
|
||||||
return this.form.cleanings && Object.keys(this.form.cleanings).length > 0
|
return this.form.cleanings && Object.keys(this.form.cleanings).length > 0
|
||||||
},
|
},
|
||||||
cleanings() {
|
cleanings () {
|
||||||
return this.form.cleanings
|
return this.form.cleanings
|
||||||
},
|
},
|
||||||
cleaningContent() {
|
cleaningContent () {
|
||||||
let message = ""
|
let message = ''
|
||||||
Object.keys(this.cleanings).forEach((key) => {
|
Object.keys(this.cleanings).forEach((key) => {
|
||||||
let fieldName = key.charAt(0).toUpperCase() + key.slice(1)
|
let fieldName = key.charAt(0).toUpperCase() + key.slice(1)
|
||||||
if (fieldName !== "Form") {
|
if (fieldName !== 'Form') {
|
||||||
fieldName = '"' + fieldName + '" field'
|
fieldName = '"' + fieldName + '" field'
|
||||||
}
|
}
|
||||||
let fieldInfo =
|
let fieldInfo = '<br><span class="font-semibold">' + fieldName + '</span><br/><ul class=\'list-disc list-inside\'>'
|
||||||
'<span class="font-semibold">' +
|
|
||||||
fieldName +
|
|
||||||
"</span><br/><ul class='list-disc list-inside'>"
|
|
||||||
this.cleanings[key].forEach((msg) => {
|
this.cleanings[key].forEach((msg) => {
|
||||||
fieldInfo = fieldInfo + "<li>" + msg + "</li>"
|
if (!msg) return
|
||||||
|
fieldInfo = fieldInfo + '<li>' + msg + '</li>'
|
||||||
})
|
})
|
||||||
if (fieldInfo) {
|
if (fieldInfo) {
|
||||||
message = message + fieldInfo + "<ul/><br/>"
|
message = message + fieldInfo + '</ul>'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return message
|
return message
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
watch: {},
|
|
||||||
mounted() {},
|
|
||||||
methods: {
|
methods: {
|
||||||
openCrisp() {
|
onUpgradeClick () {
|
||||||
useCrisp().openAndShowChat()
|
this.subscriptionModalStore.setModalContent('Upgrade to unlock all features for your form', 'Some features are disabled on the published form. Upgrade your plan to unlock these features and much more. Gain full access to all advanced features.')
|
||||||
},
|
this.subscriptionModalStore.openModal()
|
||||||
},
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,33 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="border border-gray-300 rounded-xl flex p-1 relative">
|
<div class="bg-slate-100 rounded-xl flex p-1 relative">
|
||||||
<button class="font-semibold block flex-grow cursor-pointer">
|
<button class="font-medium block flex-grow cursor-pointer">
|
||||||
<div
|
<div
|
||||||
class="p-2 px-3 rounded-lg transition-colors"
|
class="py-1.5 px-3 rounded-lg transition-colors transition-colors text-slate-500"
|
||||||
:class="{ 'bg-blue-500 text-white': !modelValue }"
|
:class="{ 'bg-white shadow text-slate-900': !modelValue }"
|
||||||
@click="set(false)"
|
@click="set(false)"
|
||||||
>
|
>
|
||||||
Monthly
|
Monthly
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="font-semibold block flex-grow cursor-pointer"
|
class="font-medium block flex-grow cursor-pointer"
|
||||||
@click="set(true)"
|
@click="set(true)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="p-2 px-4 rounded-lg transition-colors"
|
class="py-1.5 px-4 rounded-lg transition-colors text-slate-500"
|
||||||
:class="{ 'bg-blue-500 text-white': modelValue }"
|
:class="{ 'bg-white shadow text-slate-900': modelValue }"
|
||||||
>
|
>
|
||||||
Yearly
|
Yearly
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div class="absolute hidden sm:block text-gray-500 text-xs mt-12">
|
<div
|
||||||
Save 20% with annual plans
|
class="absolute hidden sm:block -right-4 -top-3 translate-x-full translate-y-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="justify-center px-2 py-1 text-xs font-semibold tracking-wide text-center text-emerald-600 uppercase bg-emerald-50 rounded-md"
|
||||||
|
>
|
||||||
|
Save 20%
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@
|
||||||
v-else
|
v-else
|
||||||
class="mr-1"
|
class="mr-1"
|
||||||
:arrow="true"
|
:arrow="true"
|
||||||
@click.prevent="openCustomerCheckout('default')"
|
@click.prevent="subscriptionModalStore.openModal('default', isYearly)"
|
||||||
>
|
>
|
||||||
Start free trial
|
Start free trial
|
||||||
</v-button>
|
</v-button>
|
||||||
|
|
@ -169,13 +169,6 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<custom-plan v-if="!homePage" />
|
<custom-plan v-if="!homePage" />
|
||||||
|
|
||||||
<checkout-details-modal
|
|
||||||
:show="showDetailsModal"
|
|
||||||
:yearly="isYearly"
|
|
||||||
:plan="selectedPlan"
|
|
||||||
@close="showDetailsModal = false"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -183,7 +176,6 @@
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import { useAuthStore } from "../../../stores/auth"
|
import { useAuthStore } from "../../../stores/auth"
|
||||||
import MonthlyYearlySelector from "./MonthlyYearlySelector.vue"
|
import MonthlyYearlySelector from "./MonthlyYearlySelector.vue"
|
||||||
import CheckoutDetailsModal from "./CheckoutDetailsModal.vue"
|
|
||||||
import CustomPlan from "./CustomPlan.vue"
|
import CustomPlan from "./CustomPlan.vue"
|
||||||
|
|
||||||
MonthlyYearlySelector.compatConfig = { MODE: 3 }
|
MonthlyYearlySelector.compatConfig = { MODE: 3 }
|
||||||
|
|
@ -191,7 +183,6 @@ MonthlyYearlySelector.compatConfig = { MODE: 3 }
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
MonthlyYearlySelector,
|
MonthlyYearlySelector,
|
||||||
CheckoutDetailsModal,
|
|
||||||
CustomPlan,
|
CustomPlan,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -201,17 +192,16 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
|
const subscriptionModalStore = useSubscriptionModalStore()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
return {
|
return {
|
||||||
|
subscriptionModalStore,
|
||||||
authenticated: computed(() => authStore.check),
|
authenticated: computed(() => authStore.check),
|
||||||
user: computed(() => authStore.user),
|
user: computed(() => authStore.user),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
isYearly: true,
|
isYearly: true,
|
||||||
selectedPlan: "pro",
|
|
||||||
showDetailsModal: false,
|
|
||||||
|
|
||||||
pricingInfo: [
|
pricingInfo: [
|
||||||
"Form confirmation emails",
|
"Form confirmation emails",
|
||||||
"Slack notifications",
|
"Slack notifications",
|
||||||
|
|
@ -228,10 +218,6 @@ export default {
|
||||||
computed: {},
|
computed: {},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
openCustomerCheckout(plan) {
|
|
||||||
this.selectedPlan = plan
|
|
||||||
this.showDetailsModal = true
|
|
||||||
},
|
|
||||||
openBilling() {
|
openBilling() {
|
||||||
this.billingLoading = true
|
this.billingLoading = true
|
||||||
opnFetch("/subscription/billing-portal").then((data) => {
|
opnFetch("/subscription/billing-portal").then((data) => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,783 @@
|
||||||
|
<template>
|
||||||
|
<modal
|
||||||
|
:show="subscriptionModalStore.show"
|
||||||
|
compact-header
|
||||||
|
max-width="screen-lg"
|
||||||
|
backdrop-blur
|
||||||
|
@close="subscriptionModalStore.closeModal()"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="currentStep === 1"
|
||||||
|
class="flex flex-col items-center pr-4 pb-20 pl-6 rounded-2xl max-md:pl-5 relative"
|
||||||
|
>
|
||||||
|
<main class="flex flex-col mt-4 max-w-full text-center w-[591px] max-md:mt-10">
|
||||||
|
<svg
|
||||||
|
class="self-center max-w-full aspect-[0.98] w-[107px]"
|
||||||
|
viewBox="0 0 108 109"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g opacity="0.5">
|
||||||
|
<path
|
||||||
|
d="M94.5371 90H95.5371V89V22C95.5371 18.134 92.4031 15 88.5371 15H37.5371C33.6711 15 30.5371 18.134 30.5371 22V89V90H31.5371H94.5371Z"
|
||||||
|
fill="url(#paint0_linear_6894_874)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M94.5371 90H95.5371V89V22C95.5371 18.134 92.4031 15 88.5371 15H37.5371C33.6711 15 30.5371 18.134 30.5371 22V89V90H31.5371H94.5371Z"
|
||||||
|
stroke="url(#paint1_linear_6894_874)"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="39.5371"
|
||||||
|
y="24"
|
||||||
|
width="47"
|
||||||
|
height="4"
|
||||||
|
rx="2"
|
||||||
|
fill="url(#paint2_linear_6894_874)"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="39.5371"
|
||||||
|
y="34"
|
||||||
|
width="47"
|
||||||
|
height="4"
|
||||||
|
rx="2"
|
||||||
|
fill="url(#paint3_linear_6894_874)"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="39.5371"
|
||||||
|
y="44"
|
||||||
|
width="24"
|
||||||
|
height="4"
|
||||||
|
rx="2"
|
||||||
|
fill="url(#paint4_linear_6894_874)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
d="M78.5371 96H79.5371V95V28C79.5371 24.134 76.4031 21 72.5371 21H21.5371C17.6711 21 14.5371 24.134 14.5371 28V95V96H15.5371H78.5371Z"
|
||||||
|
fill="url(#paint5_linear_6894_874)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M78.5371 96H79.5371V95V28C79.5371 24.134 76.4031 21 72.5371 21H21.5371C17.6711 21 14.5371 24.134 14.5371 28V95V96H15.5371H78.5371Z"
|
||||||
|
stroke="url(#paint6_linear_6894_874)"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="23.5371"
|
||||||
|
y="30"
|
||||||
|
width="47"
|
||||||
|
height="4"
|
||||||
|
rx="2"
|
||||||
|
fill="url(#paint7_linear_6894_874)"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="23.5371"
|
||||||
|
y="40"
|
||||||
|
width="47"
|
||||||
|
height="4"
|
||||||
|
rx="2"
|
||||||
|
fill="url(#paint8_linear_6894_874)"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="23.5371"
|
||||||
|
y="50"
|
||||||
|
width="24"
|
||||||
|
height="4"
|
||||||
|
rx="2"
|
||||||
|
fill="url(#paint9_linear_6894_874)"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
transform="translate(0.5 9)"
|
||||||
|
fill="url(#paint10_linear_6894_874)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M89.7665 18.8875L88.75 22.236L87.7335 18.8875C87.1996 17.1288 85.7389 15.754 83.8702 15.2515L80.3125 14.2948L83.8702 13.3381C85.7389 12.8356 87.1996 11.4608 87.7335 9.70209L88.75 6.35363L89.7665 9.70209C90.3004 11.4608 91.7611 12.8356 93.6298 13.3381L97.1875 14.2948L93.6298 15.2515C91.7611 15.754 90.3004 17.1288 89.7665 18.8875Z"
|
||||||
|
stroke="url(#paint11_linear_6894_874)"
|
||||||
|
stroke-width="1.25"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M100.324 10.4296L100 11.6477L99.6764 10.4296C99.2985 9.00696 98.1183 7.89618 96.6068 7.54053L95.3125 7.23598L96.6068 6.93144C98.1183 6.57579 99.2985 5.46501 99.6764 4.04241L100 2.82422L100.324 4.04241C100.701 5.46501 101.882 6.57579 103.393 6.93144L104.688 7.23598L103.393 7.54053C101.882 7.89618 100.701 9.00695 100.324 10.4296Z"
|
||||||
|
stroke="url(#paint12_linear_6894_874)"
|
||||||
|
stroke-width="1.25"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M98.6178 24.3739L98.125 25.7654L97.6322 24.3739C97.3523 23.5835 96.6932 22.9633 95.8534 22.6998L94.375 22.236L95.8534 21.7722C96.6932 21.5087 97.3523 20.8884 97.6322 20.098L98.125 18.7066L98.6178 20.098C98.8977 20.8884 99.5568 21.5087 100.397 21.7722L101.875 22.236L100.397 22.6998C99.5568 22.9633 98.8977 23.5835 98.6178 24.3739Z"
|
||||||
|
stroke="url(#paint13_linear_6894_874)"
|
||||||
|
stroke-width="1.25"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="paint0_linear_6894_874"
|
||||||
|
x1="31.6431"
|
||||||
|
y1="17.1538"
|
||||||
|
x2="97.3504"
|
||||||
|
y2="81.7277"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#F5F8FB" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="white"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint1_linear_6894_874"
|
||||||
|
x1="31.2928"
|
||||||
|
y1="93.4417"
|
||||||
|
x2="93.1583"
|
||||||
|
y2="93.3534"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#CBD5E1" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#EAEEF2"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint2_linear_6894_874"
|
||||||
|
x1="39.3548"
|
||||||
|
y1="28.2434"
|
||||||
|
x2="85.4911"
|
||||||
|
y2="27.347"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#CBD5E1" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#EAEEF2"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint3_linear_6894_874"
|
||||||
|
x1="39.3548"
|
||||||
|
y1="38.2434"
|
||||||
|
x2="85.4911"
|
||||||
|
y2="37.347"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#CBD5E1" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#EAEEF2"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint4_linear_6894_874"
|
||||||
|
x1="39.444"
|
||||||
|
y1="48.2434"
|
||||||
|
x2="63.0096"
|
||||||
|
y2="48.0096"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#CBD5E1" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#EAEEF2"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint5_linear_6894_874"
|
||||||
|
x1="15.6431"
|
||||||
|
y1="23.1538"
|
||||||
|
x2="81.3504"
|
||||||
|
y2="87.7277"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#F5F8FB" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="white"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint6_linear_6894_874"
|
||||||
|
x1="15.2928"
|
||||||
|
y1="99.4417"
|
||||||
|
x2="77.1583"
|
||||||
|
y2="99.3534"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#CBD5E1" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#EAEEF2"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint7_linear_6894_874"
|
||||||
|
x1="23.3548"
|
||||||
|
y1="34.2434"
|
||||||
|
x2="69.4911"
|
||||||
|
y2="33.347"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#CBD5E1" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#EAEEF2"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint8_linear_6894_874"
|
||||||
|
x1="23.3548"
|
||||||
|
y1="44.2434"
|
||||||
|
x2="69.4911"
|
||||||
|
y2="43.347"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#CBD5E1" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#EAEEF2"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint9_linear_6894_874"
|
||||||
|
x1="23.444"
|
||||||
|
y1="54.2434"
|
||||||
|
x2="47.0096"
|
||||||
|
y2="54.0096"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#CBD5E1" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#EAEEF2"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint10_linear_6894_874"
|
||||||
|
x1="50"
|
||||||
|
y1="0"
|
||||||
|
x2="50"
|
||||||
|
y2="100"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset="0.53"
|
||||||
|
stop-color="white"
|
||||||
|
stop-opacity="0"
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset="0.84"
|
||||||
|
stop-color="white"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint11_linear_6894_874"
|
||||||
|
x1="92.5"
|
||||||
|
y1="2.82422"
|
||||||
|
x2="92.5"
|
||||||
|
y2="25.7654"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#B4D5FF" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#2B88FD"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint12_linear_6894_874"
|
||||||
|
x1="92.5"
|
||||||
|
y1="2.82422"
|
||||||
|
x2="92.5"
|
||||||
|
y2="25.7654"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#B4D5FF" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#2B88FD"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint13_linear_6894_874"
|
||||||
|
x1="92.5"
|
||||||
|
y1="2.82422"
|
||||||
|
x2="92.5"
|
||||||
|
y2="25.7654"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#B4D5FF" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#2B88FD"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<section class="flex flex-col mt-2 max-md:max-w-full">
|
||||||
|
<h1
|
||||||
|
class="justify-center self-center text-2xl font-bold tracking-tight leading-9 text-slate-800 max-md:max-w-full"
|
||||||
|
>
|
||||||
|
{{ subscriptionModalStore.modal_title }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-4 text-base leading-6 text-slate-500 max-md:max-w-full">
|
||||||
|
{{ subscriptionModalStore.modal_description }}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<div class="mt-8 mb-4 flex items-center justify-center">
|
||||||
|
<MonthlyYearlySelector
|
||||||
|
v-model="isYearly"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<section class="flex flex-col w-full max-w-[800px] max-md:max-w-full">
|
||||||
|
<div class="bg-white max-md:max-w-full">
|
||||||
|
<div class="flex gap-2 max-md:flex-col max-md:gap-0">
|
||||||
|
<article
|
||||||
|
v-if="!isSubscribed"
|
||||||
|
class="flex flex-col w-6/12 max-md:ml-0 max-md:w-full m-auto"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col grow justify-between p-6 w-full bg-blue-50 rounded-2xl max-md:px-5 max-md:mt-2">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex gap-2 py-px">
|
||||||
|
<h2 class="my-auto text-lg font-medium tracking-tighter leading-5 text-slate-900">
|
||||||
|
Pro
|
||||||
|
</h2>
|
||||||
|
<span
|
||||||
|
v-if="isYearly"
|
||||||
|
class="justify-center px-2 py-1 text-xs font-semibold tracking-wide text-center text-emerald-600 uppercase bg-emerald-50 rounded-md"
|
||||||
|
>
|
||||||
|
Save 20%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col justify-end mt-4 leading-[100%]">
|
||||||
|
<p class="text-2xl font-semibold tracking-tight text-slate-900">
|
||||||
|
<template v-if="isYearly">
|
||||||
|
$16
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
$19
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-slate-500">
|
||||||
|
per month, billed
|
||||||
|
<template v-if="isYearly">
|
||||||
|
yearly
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
monthly
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-sm leading-5 text-slate-500">
|
||||||
|
For companies and more customization on their forms.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<v-button
|
||||||
|
v-if="!user?.is_subscribed"
|
||||||
|
v-track.upgrade_modal_start_trial="{plan: 'default', period: isYearly?'yearly':'monthly'}"
|
||||||
|
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"
|
||||||
|
@click.prevent="onSelectPlan('default')"
|
||||||
|
>
|
||||||
|
Start 3-day trial
|
||||||
|
</v-button>
|
||||||
|
<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"
|
||||||
|
target="_blank"
|
||||||
|
@click="openBillingDashboard"
|
||||||
|
>
|
||||||
|
Manage Plan
|
||||||
|
</v-button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="flex flex-col self-stretch mt-12 max-md:mt-10 max-md:max-w-full">
|
||||||
|
<div class="justify-center max-md:pr-5 max-md:max-w-full">
|
||||||
|
<div class="flex gap-5 max-md:flex-col max-md:gap-0">
|
||||||
|
<article class="flex flex-col w-[33%] max-md:ml-0 max-md:w-full">
|
||||||
|
<div class="flex flex-col grow text-base leading-6 text-slate-500 max-md:mt-10">
|
||||||
|
<Icon
|
||||||
|
name="mdi:star-outline"
|
||||||
|
class="w-5 h-5 text-nt-blue"
|
||||||
|
/>
|
||||||
|
<p class="mt-2">
|
||||||
|
<strong class="font-semibold text-slate-800">Remove OpnForm branding.</strong>
|
||||||
|
<span class="text-slate-500"> Remove our watermark, create forms that match your brand.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="flex flex-col ml-5 w-[33%] max-md:ml-0 max-md:w-full">
|
||||||
|
<div class="flex flex-col grow text-base leading-6 text-slate-500 max-md:mt-10">
|
||||||
|
<Icon
|
||||||
|
name="ion:brush-outline"
|
||||||
|
class="w-5 h-5 text-nt-blue"
|
||||||
|
/>
|
||||||
|
<p class="mt-2">
|
||||||
|
<strong class="font-semibold text-slate-800">Full form customization.</strong>
|
||||||
|
<span class="text-slate-500"> Customize the colors, themes, images etc of your forms. Inject custom CSS and JS code.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="flex flex-col w-[33%] max-md:ml-0 max-md:w-full">
|
||||||
|
<div class="flex flex-col grow text-base leading-6 text-slate-500 max-md:mt-10">
|
||||||
|
<Icon
|
||||||
|
name="icons8:upload-2"
|
||||||
|
class="w-5 h-5 text-nt-blue"
|
||||||
|
/>
|
||||||
|
<p class="mt-2">
|
||||||
|
<strong class="font-semibold text-slate-800">Larger File uploads.</strong>
|
||||||
|
<span class="text-slate-500"> Larger files upload in your forms (up to 50 mb). This allows you to collect bigger attachments.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="justify-center mt-12 max-md:pr-5 max-md:mt-10 max-md:max-w-full">
|
||||||
|
<div class="flex gap-5 max-md:flex-col max-md:gap-0">
|
||||||
|
<article class="flex flex-col w-[33%] max-md:ml-0 max-md:w-full">
|
||||||
|
<div class="flex flex-col grow text-base leading-6 text-slate-500 max-md:mt-10">
|
||||||
|
<Icon
|
||||||
|
name="heroicons:bell"
|
||||||
|
class="w-5 h-5 text-nt-blue"
|
||||||
|
/>
|
||||||
|
<p class="mt-2">
|
||||||
|
<strong class="font-semibold text-slate-800">Access to all integrations.</strong>
|
||||||
|
<span class="text-slate-500"> Setup email, Slack, Discord notifications or GSheet, Zapier or webhooks integrations.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="flex flex-col ml-5 w-[33%] max-md:ml-0 max-md:w-full">
|
||||||
|
<div class="flex flex-col grow text-base leading-6 text-slate-500 max-md:mt-10">
|
||||||
|
<Icon
|
||||||
|
name="heroicons:globe-alt"
|
||||||
|
class="w-5 h-5 text-nt-blue"
|
||||||
|
/>
|
||||||
|
<p class="mt-2">
|
||||||
|
<strong class="font-semibold text-slate-800">1 custom domain.</strong>
|
||||||
|
<span class="text-slate-500"> Host your form on your own domain for a professional look and improved branding.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="flex flex-col ml-5 w-[33%] max-md:ml-0 max-md:w-full">
|
||||||
|
<div class="flex flex-col grow text-base leading-6 text-slate-500 max-md:mt-10">
|
||||||
|
<Icon
|
||||||
|
name="mdi:pencil-outline"
|
||||||
|
class="w-5 h-5 text-nt-blue"
|
||||||
|
/>
|
||||||
|
<p class="mt-2">
|
||||||
|
<strong class="font-semibold text-slate-800">Editable submissions.</strong>
|
||||||
|
<span class="text-slate-500"> Form respondents can go back and edit their form submissions, allowing for updates and corrections.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer
|
||||||
|
class="justify-center py-1.5 mt-12 text-base font-medium leading-6 text-center text-blue-500 max-md:mt-10"
|
||||||
|
>
|
||||||
|
<NuxtLink
|
||||||
|
:to="{ name: 'pricing' }"
|
||||||
|
target="_blank"
|
||||||
|
class="focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
And much more. See full plans comparison
|
||||||
|
<Icon
|
||||||
|
class="h-6 w-5"
|
||||||
|
name="heroicons:arrow-small-right"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<section
|
||||||
|
v-else-if="currentStep === 2"
|
||||||
|
class="flex flex-col items-center px-6 pb-4 bg-white rounded-2xl w-full"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2 max-md:flex-wrap">
|
||||||
|
<div class="flex justify-center items-center p-2 rounded-[1000px]">
|
||||||
|
<Icon
|
||||||
|
name="heroicons:chevron-left-16-solid"
|
||||||
|
class="h-6 w-6 cursor-pointer"
|
||||||
|
@click.prevent="currentStep=1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 class="flex-1 my-auto text-xl font-bold leading-8 text-center text-slate-800 max-md:max-w-full">
|
||||||
|
Confirm
|
||||||
|
<template v-if="isSubscribed">
|
||||||
|
Upgrade
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
Subscription
|
||||||
|
</template>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow w-full max-w-sm">
|
||||||
|
<div
|
||||||
|
v-if="!isSubscribed"
|
||||||
|
class="bg-blue-50 rounded-md p-4 border border-blue-200 flex flex-col my-4 gap-1"
|
||||||
|
>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<p class="text-blue-500 capitalize font-medium flex-grow">
|
||||||
|
OpnForm - {{ currentPlan == 'default' ? 'Pro' : 'Team' }} plan
|
||||||
|
</p>
|
||||||
|
<UBadge
|
||||||
|
:color="isYearly?'green':'amber'"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{{ !isYearly ? 'No Discount' : 'Discount Applied' }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm leading-5 text-slate-500">
|
||||||
|
<span
|
||||||
|
v-if="isYearly"
|
||||||
|
class="font-medium line-through mr-2"
|
||||||
|
v-text="'$19'"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="font-medium"
|
||||||
|
:class="{'text-green-700':isYearly}"
|
||||||
|
v-text="isYearly ? '$16' : '$19'"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="text-xs"
|
||||||
|
:class="{'text-green-700':isYearly}"
|
||||||
|
>
|
||||||
|
per month, billed
|
||||||
|
<template v-if="isYearly">
|
||||||
|
yearly
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
monthly
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div v-if="shouldShowUpsell">
|
||||||
|
<v-form size="sm">
|
||||||
|
<toggle-switch-input
|
||||||
|
v-model="isYearly"
|
||||||
|
label="20% off with the yearly plan"
|
||||||
|
size="sm"
|
||||||
|
wrapper-class="mb-0"
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<text-input
|
||||||
|
ref="companyName"
|
||||||
|
label="Company Name"
|
||||||
|
name="name"
|
||||||
|
:required="true"
|
||||||
|
:form="form"
|
||||||
|
help="Name that will appear on invoices"
|
||||||
|
/>
|
||||||
|
<text-input
|
||||||
|
label="Invoicing Email"
|
||||||
|
name="email"
|
||||||
|
native-type="email"
|
||||||
|
:required="true"
|
||||||
|
:form="form"
|
||||||
|
help="Where invoices will be sent"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="flex gap-2 mt-6 w-full"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
v-track.upgrade_modal_confirm_submit="{plan: currentPlan.value, period: isYearly?'yearly':'monthly'}"
|
||||||
|
block
|
||||||
|
size="md"
|
||||||
|
class="w-auto flex-grow"
|
||||||
|
:loading="form.busy || loading"
|
||||||
|
@click="saveDetails"
|
||||||
|
>
|
||||||
|
<template v-if="isSubscribed">
|
||||||
|
Upgrade
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
Subscribe
|
||||||
|
</template>
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
size="md"
|
||||||
|
color="white"
|
||||||
|
@click="currentStep=1"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { fetchAllWorkspaces } from '~/stores/workspaces.js'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const subscriptionModalStore = useSubscriptionModalStore()
|
||||||
|
|
||||||
|
const currentPlan = ref(subscriptionModalStore.plan || 'default')
|
||||||
|
const currentStep = ref(1)
|
||||||
|
const isYearly = ref(subscriptionModalStore.yearly)
|
||||||
|
const loading = ref(false)
|
||||||
|
const billingLoading = ref(false)
|
||||||
|
const shouldShowUpsell = ref(false)
|
||||||
|
const form = useForm({
|
||||||
|
name: '',
|
||||||
|
email: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const subscribeBroadcast = useBroadcastChannel('subscribe')
|
||||||
|
const broadcastData = subscribeBroadcast.data
|
||||||
|
const confetti = useConfetti()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const workspacesStore = useWorkspacesStore()
|
||||||
|
const authenticated = computed(() => authStore.check)
|
||||||
|
const user = computed(() => authStore.user)
|
||||||
|
const isSubscribed = computed(() => workspacesStore.isSubscribed)
|
||||||
|
const currency = 'usd'
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
isYearly.value = subscriptionModalStore.yearly
|
||||||
|
shouldShowUpsell.value = !isYearly.value
|
||||||
|
currentStep.value = 2
|
||||||
|
currentPlan.value = subscriptionModalStore.plan
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(broadcastData, () => {
|
||||||
|
if (import.meta.server || !subscriptionModalStore.show || !broadcastData.value || !broadcastData.value.type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (broadcastData.value.type === 'success') {
|
||||||
|
// Now we need to reload workspace and user
|
||||||
|
opnFetch('user').then((userData) => {
|
||||||
|
authStore.setUser(userData)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const eventData = {
|
||||||
|
plan: user.value.has_enterprise_subscription ? 'enterprise' : 'pro'
|
||||||
|
}
|
||||||
|
useAmplitude().logEvent('subscribed', eventData)
|
||||||
|
useCrisp().pushEvent('subscribed', eventData)
|
||||||
|
useGtm().trackEvent({ event: 'subscribed', ...eventData })
|
||||||
|
if (import.meta.client && window.rewardful) {
|
||||||
|
window.rewardful('convert', { email: user.value.email })
|
||||||
|
}
|
||||||
|
console.log('Subscription registered 🎊')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to register subscription event 😔',error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
fetchAllWorkspaces().then((workspaces) => {
|
||||||
|
workspacesStore.set(workspaces.data.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (user.value.has_enterprise_subscription) {
|
||||||
|
useAlert().success(
|
||||||
|
'Awesome! Your subscription to OpnForm is now confirmed! You now have access to all Team '
|
||||||
|
+ '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 🙌'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
confetti.play()
|
||||||
|
subscriptionModalStore.closeModal()
|
||||||
|
} else {
|
||||||
|
useAlert().error(
|
||||||
|
'Unfortunately we could not confirm your subscription. Please try again and contact us if the issue persists.'
|
||||||
|
)
|
||||||
|
currentStep.value = 1
|
||||||
|
shouldShowUpsell.value = true
|
||||||
|
}
|
||||||
|
subscribeBroadcast.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (user && user.value) {
|
||||||
|
form.name = user.value.name
|
||||||
|
form.email = user.value.email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const onSelectPlan = (planName) => {
|
||||||
|
if (!authenticated.value) {
|
||||||
|
subscriptionModalStore.closeModal()
|
||||||
|
router.push({ name: "register" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
currentPlan.value = planName
|
||||||
|
shouldShowUpsell.value = !isYearly.value
|
||||||
|
currentStep.value = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
@ -16,7 +16,7 @@ export function useAmplitude () {
|
||||||
}
|
}
|
||||||
|
|
||||||
const logEvent = function (eventName, eventData) {
|
const logEvent = function (eventName, eventData) {
|
||||||
if (!config.public.env === 'production') {
|
if (config.public.env !== 'production') {
|
||||||
console.log('[DEBUG] Amplitude logged event:', eventName, eventData)
|
console.log('[DEBUG] Amplitude logged event:', eventName, eventData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -185,28 +185,28 @@
|
||||||
class="px-4"
|
class="px-4"
|
||||||
>
|
>
|
||||||
<UAlert
|
<UAlert
|
||||||
class="mt-4"
|
class="mt-8 p-4"
|
||||||
icon="i-heroicons-command-line"
|
icon="i-heroicons-sparkles"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
description="You can add components to your app using the cli."
|
description="You can add components to your app using the cli."
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<h2 class="font-medium text-lg -mt-2">
|
<h3 class="font-semibold text-md">
|
||||||
Discover our Pro plan
|
Discover our Pro plan
|
||||||
</h2>
|
</h3>
|
||||||
</template>
|
</template>
|
||||||
<template #description>
|
<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">
|
<p class="flex-grow">
|
||||||
Remove NoteForms branding, customize forms further, use your custom domain, integrate with your
|
Remove NoteForms branding, customize forms further, use your custom domain, integrate with your
|
||||||
favorite tools, invite users, and more!
|
favorite tools, invite users, and more!
|
||||||
</p>
|
</p>
|
||||||
<UButton
|
<UButton
|
||||||
v-track.upgrade_banner_home_click
|
v-track.upgrade_banner_home_click
|
||||||
:to="{name:'pricing'}"
|
|
||||||
color="white"
|
color="white"
|
||||||
class="block"
|
class="block"
|
||||||
|
@click.prevent="subscriptionModalStore.openModal()"
|
||||||
>
|
>
|
||||||
Upgrade Now
|
Upgrade Now
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -246,6 +246,7 @@ useOpnSeoMeta({
|
||||||
"All of your OpnForm are here. Create new forms, or update your existing forms.",
|
"All of your OpnForm are here. Create new forms, or update your existing forms.",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const subscriptionModalStore = useSubscriptionModalStore()
|
||||||
const formsStore = useFormsStore()
|
const formsStore = useFormsStore()
|
||||||
const workspacesStore = useWorkspacesStore()
|
const workspacesStore = useWorkspacesStore()
|
||||||
formsStore.startLoading()
|
formsStore.startLoading()
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,18 @@
|
||||||
:loading="billingLoading"
|
:loading="billingLoading"
|
||||||
@click="openBillingDashboard"
|
@click="openBillingDashboard"
|
||||||
>
|
>
|
||||||
Manage Subscription
|
<span v-if="canCancel">Manage Subscription & Invoices</span>
|
||||||
|
<span else>Billing & Invoices</span>
|
||||||
</UButton>
|
</UButton>
|
||||||
|
<br><br>
|
||||||
|
<v-button
|
||||||
|
v-if="canCancel"
|
||||||
|
color="white"
|
||||||
|
:loading="cancelLoading"
|
||||||
|
@click.prevent="cancelSubscription"
|
||||||
|
>
|
||||||
|
Cancel Subscription
|
||||||
|
</v-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -45,6 +55,7 @@ definePageMeta({
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const user = computed(() => authStore.user)
|
const user = computed(() => authStore.user)
|
||||||
const billingLoading = ref(false)
|
const billingLoading = ref(false)
|
||||||
|
const cancelLoading = ref(false)
|
||||||
const usersCount = ref(0)
|
const usersCount = ref(0)
|
||||||
|
|
||||||
onMounted(() => {
|
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 = () => {
|
const openBillingDashboard = () => {
|
||||||
billingLoading.value = true
|
billingLoading.value = true
|
||||||
opnFetch("/subscription/billing-portal")
|
opnFetch('/subscription/billing-portal').then((data) => {
|
||||||
.then((data) => {
|
const url = data.portal_url
|
||||||
const url = data.portal_url
|
window.location = url
|
||||||
window.location = url
|
}).catch((error) => {
|
||||||
})
|
useAlert().error(error.data.message)
|
||||||
.catch((error) => {
|
}).finally(() => {
|
||||||
useAlert().error(error.data.message)
|
billingLoading.value = false
|
||||||
})
|
})
|
||||||
.finally(() => {
|
|
||||||
billingLoading.value = false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,14 @@
|
||||||
<template />
|
<script setup>
|
||||||
|
import { useBroadcastChannel } from '@vueuse/core'
|
||||||
|
|
||||||
<script>
|
definePageMeta({
|
||||||
import { computed } from "vue"
|
middleware: 'auth'
|
||||||
import { useAuthStore } from "../../stores/auth"
|
})
|
||||||
|
|
||||||
export default {
|
const subscribeBroadcast = useBroadcastChannel('subscribe')
|
||||||
components: {},
|
|
||||||
layout: "default",
|
|
||||||
middleware: "auth",
|
|
||||||
|
|
||||||
setup() {
|
onMounted(() => {
|
||||||
useOpnSeoMeta({
|
subscribeBroadcast.post({ 'type': 'error' })
|
||||||
title: "Error",
|
window.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
@ -17,86 +17,55 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import {computed} from "vue"
|
|
||||||
import {useAuthStore} from "../../stores/auth"
|
|
||||||
|
|
||||||
export default {
|
<script setup>
|
||||||
layout: "default",
|
import { useBroadcastChannel } from '@vueuse/core'
|
||||||
middleware: "auth",
|
|
||||||
|
|
||||||
setup() {
|
definePageMeta({
|
||||||
useOpnSeoMeta({
|
middleware: 'auth'
|
||||||
title: "Subscription Success",
|
})
|
||||||
})
|
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
useOpnSeoMeta({
|
||||||
const confetti = useConfetti()
|
title: 'Subscription Success'
|
||||||
return {
|
})
|
||||||
authStore,
|
|
||||||
confetti,
|
|
||||||
authenticated: computed(() => authStore.check),
|
|
||||||
user: computed(() => authStore.user),
|
|
||||||
crisp: useCrisp(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data: () => ({
|
const authStore = useAuthStore()
|
||||||
interval: null,
|
const confetti = useConfetti()
|
||||||
}),
|
const user = computed(() => authStore.user)
|
||||||
|
const subscribeBroadcast = useBroadcastChannel('subscribe')
|
||||||
|
|
||||||
computed: {},
|
const interval = ref(null)
|
||||||
|
|
||||||
mounted() {
|
const redirectIfSubscribed = () => {
|
||||||
this.redirectIfSubscribed()
|
if (user.value.is_subscribed) {
|
||||||
this.interval = setInterval(() => this.checkSubscription(), 5000)
|
subscribeBroadcast.post({ 'type': 'success' })
|
||||||
},
|
window.close()
|
||||||
|
|
||||||
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 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>
|
</script>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
const DEFAULT_MODAL_TITLE = 'Upgrade and take your forms to the next level'
|
||||||
|
const DEFAULT_MODAL_DESCRIPTION = 'On the Free plan, you can try out all paid features only within the form editor. Upgrade your plan to gain full access to all features.'
|
||||||
|
|
||||||
|
export const useSubscriptionModalStore = defineStore('subscription_modal', {
|
||||||
|
state: () => ({
|
||||||
|
show: false,
|
||||||
|
plan: null,
|
||||||
|
yearly: true,
|
||||||
|
modal_title: DEFAULT_MODAL_TITLE,
|
||||||
|
modal_description: DEFAULT_MODAL_DESCRIPTION
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
openModal (planName = undefined, isYearly = undefined) {
|
||||||
|
this.plan = (planName !== undefined) ? planName : null
|
||||||
|
this.yearly = (isYearly !== undefined) ? isYearly : true
|
||||||
|
this.show = true
|
||||||
|
},
|
||||||
|
setModalContent (title = null, description = null) {
|
||||||
|
this.modal_title = title || DEFAULT_MODAL_TITLE
|
||||||
|
this.modal_description = description || DEFAULT_MODAL_DESCRIPTION
|
||||||
|
},
|
||||||
|
closeModal () {
|
||||||
|
this.show = false
|
||||||
|
this.plan = null
|
||||||
|
this.modal_title = DEFAULT_MODAL_TITLE
|
||||||
|
this.modal_description = DEFAULT_MODAL_DESCRIPTION
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue