Enable pricing (#151)

* Enable Pro plan - WIP

* no pricing page if have no paid plans

* Set pricing ids in env

* views & submissions FREE for all

* extra param for env

* form password FREE for all

* Custom Code is PRO feature

* Replace codeinput prism with codemirror

* Better form Cleaning message

* Added risky user email spam protection

* fix form cleaning

* Pricing page new UI

* form cleaner

* Polish changes

* Fixed tests

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
formsdev
2023-08-30 13:28:29 +05:30
committed by GitHub
parent 29b153bd76
commit fb79a5bf3e
48 changed files with 1011 additions and 269 deletions

View File

@@ -8,9 +8,7 @@
<span
class="ml-2 text-md hidden sm:inline text-black dark:text-white"
>
{{ appName }}</span><span
class="bg-gray-100 text-gray-600 font-semibold inline-block ml-1 px-2 rounded-full text-black text-xs tracking-wider"
>BETA</span>
{{ appName }}</span>
</router-link>
<workspace-dropdown class="ml-6"/>
</div>
@@ -23,6 +21,11 @@
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8">
Templates
</router-link>
<router-link :to="{name:'pricing'}" v-if="paidPlansEnabled && (user===null || (user && workspace && !workspace.is_pro)) && $route.name !== 'pricing'"
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8">
<span v-if="user">Upgrade</span>
<span v-else>Pricing</span>
</router-link>
<a href="#" class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1"
@click.prevent="$crisp.push(['do', 'helpdesk:search'])" v-if="hasCrisp"
>
@@ -158,6 +161,12 @@ export default {
}
return null
},
workspace () {
return this.$store.getters['open/workspaces/getCurrent']()
},
paidPlansEnabled() {
return window.config.paid_plans_enabled
},
showAuth() {
return this.$route.name && !this.$route.name.startsWith('forms.show_public')
},

View File

@@ -5,8 +5,13 @@
<slot name="title" />
</div>
<div class="text-gray-400 hover:text-gray-600 absolute -right-2 -top-1 cursor-pointer p-2" @click="trigger">
<svg class="h-3 w-3 transition transform duration-500" :class="{'rotate-180':showContent}" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L7 7L13 1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 transition transform duration-500"
:class="{'rotate-180':showContent}" viewBox="0 0 20 20" fill="currentColor"
>
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v3.586L7.707 9.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 10.586V7z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
@@ -20,21 +25,23 @@
<script>
import VTransition from './transitions/VTransition.vue'
export default {
name: 'Collapse',
components: { VTransition },
props: {
defaultValue: { type: Boolean, default: false }
defaultValue: { type: Boolean, default: false },
value: { type: Boolean, default: null }
},
data () {
return {
showContent: this.defaultValue
showContent: this.value ?? this.defaultValue
}
},
methods: {
trigger () {
this.showContent = !this.showContent
this.$emit('click', this.showContent)
this.$emit('input', this.showContent)
}
}
}

View File

@@ -33,7 +33,7 @@
</p>
<p class="my-4 text-center">
Feel free to <a href="mailto:contact@opnform.com">contact us</a> if you have any feature request.
Feel free to <a href="#" @click.prevent="openChat">contact us</a> if you have any feature request.
</p>
<div class="mb-4 text-center">
<v-button color="gray" shade="light" @click="showPremiumModal=false">
@@ -66,13 +66,20 @@ export default {
currentWorkSpace: 'open/workspaces/getCurrent',
}),
shouldDisplayProTag() {
return false; //!this.user.is_subscribed && !(this.currentWorkSpace.is_pro || this.currentWorkSpace.is_enterprise);
if(!window.config.paid_plans_enabled) return false
if (!this.user) return true
return !(this.currentWorkSpace().is_pro || this.currentWorkSpace().is_enterprise)
},
},
mounted () {
},
methods: {}
methods: {
openChat () {
window.$crisp.push(['do', 'chat:show'])
window.$crisp.push(['do', 'chat:open'])
},
}
}
</script>

View File

@@ -7,13 +7,13 @@
<span v-if="required" class="text-red-500 required-dot">*</span>
</label>
<prism-editor :id="id?id:name" v-model="compVal" :disabled="disabled"
class="code-editor"
:class="[theme.CodeInput.input,{ '!ring-red-500 !ring-2': hasValidation && form.errors.has(name), '!cursor-not-allowed !bg-gray-200':disabled }]"
<div :class="[theme.CodeInput.input,{ '!ring-red-500 !ring-2': hasValidation && form.errors.has(name), '!cursor-not-allowed !bg-gray-200':disabled }]">
<codemirror :id="id?id:name" v-model="compVal" :disabled="disabled"
:options="cmOptions"
:style="inputStyle" :name="name"
:placeholder="placeholder"
:highlight="highlighter" @change="onChange"
/>
/>
</div>
<small v-if="help" :class="theme.CodeInput.help">
<slot name="help"><span class="field-help" v-html="help" /></slot>
@@ -23,31 +23,32 @@
</template>
<script>
// import Prism Editor
import { PrismEditor } from 'vue-prism-editor'
import 'vue-prism-editor/dist/prismeditor.min.css' // import the styles somewhere
// import highlighting library (you can use any library you want just return html string)
import { codemirror } from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/mode/htmlmixed/htmlmixed.js'
import { highlight, languages } from 'prismjs/components/prism-core'
import 'prismjs/components/prism-clike'
import 'prismjs/components/prism-markup'
import 'prismjs/themes/prism-tomorrow.css' // import syntax highlighting styles
import inputMixin from '~/mixins/forms/input.js'
export default {
name: 'CodeInput',
components: { PrismEditor },
components: { codemirror },
mixins: [inputMixin],
methods: {
onChange (event) {
const file = event.target.files[0]
this.$set(this.form, this.name, file)
},
highlighter (code) {
return highlight(code, languages.markup) // languages.<insert language> to return html with markup
data () {
return {
cmOptions: {
// codemirror options
tabSize: 4,
mode: 'text/html',
theme: 'default',
lineNumbers: true,
line: true
}
}
}
},
methods: {}
}
</script>

View File

@@ -5,7 +5,9 @@
</small>
<div class="flex">
<v-switch :id="id?id:name" v-model="compVal" class="inline-block mr-2" :disabled="disabled" :name="name" @input="$emit('input',$event)" />
<span>{{ label }} <span v-if="required" class="text-red-500 required-dot">*</span></span>
<slot name="label">
<span>{{ label }} <span v-if="required" class="text-red-500 required-dot">*</span></span>
</slot>
</div>
<small v-if="help && helpPosition=='below_input'" :class="theme.default.help">
<slot name="help"><span class="field-help" v-html="help" /></slot>

View File

@@ -49,23 +49,7 @@
</div>
</div>
<div v-if="getFormCleaningsMsg"
class="border shadow-sm p-2 my-4 flex items-center rounded-md bg-yellow-100 border-yellow-500"
>
<div class="flex-grow">
<p class="mb-0 py-2 px-4 text-yellow-600">
You're seeing this because you are an owner of this form. <br>
All your Pro features are de-activated when sharing this form: <br>
<span v-html="getFormCleaningsMsg" />
</p>
</div>
<div class="text-right">
<v-button color="yellow" shade="light" @click="form.cleanings=false">
Close
</v-button>
</div>
</div>
<form-cleanings v-if="!adminPreview" :hideable="true" class="mb-4 mx-2" :form="form" :specify-form-owner="true" />
<transition
v-if="!form.is_password_protected && (!isPublicFormPage || (!form.is_closed && !form.max_number_of_submissions_reached && form.visibility!='closed'))"
@@ -131,9 +115,10 @@ import { themes } from '~/config/form-themes.js'
import VButton from '../../common/Button.vue'
import VTransition from '../../common/transitions/VTransition.vue'
import FormPendingSubmissionKey from '../../../mixins/forms/form-pending-submission-key.js'
import FormCleanings from '../../pages/forms/show/FormCleanings.vue'
export default {
components: { VTransition, VButton, OpenFormButton, OpenForm },
components: { VTransition, VButton, OpenFormButton, OpenForm, FormCleanings },
props: {
form: { type: Object, required: true },
@@ -166,22 +151,6 @@ export default {
theme () {
return this.themes[this.themes.hasOwnProperty(this.form.theme) ? this.form.theme : 'default']
},
getFormCleaningsMsg () {
if (this.form.cleanings && Object.keys(this.form.cleanings).length > 0) {
let message = ''
Object.keys(this.form.cleanings).forEach((key) => {
const fieldName = key.charAt(0).toUpperCase() + key.slice(1)
let fieldInfo = '<br/>' + fieldName + "<br/><ul class='list-disc list-inside'>"
this.form.cleanings[key].forEach((msg) => {
fieldInfo = fieldInfo + '<li>' + msg + '</li>'
})
message = message + fieldInfo + '<ul/>'
})
return message
}
return false
},
isPublicFormPage () {
return this.$route.name === 'forms.show_public'
},

View File

@@ -7,8 +7,8 @@
<form v-else-if="dataForm" @submit.prevent="">
<transition name="fade" mode="out-in" appear>
<template v-for="group, groupIndex in fieldGroups">
<div v-if="currentFieldGroupIndex===groupIndex"
:key="groupIndex"
<div v-if="currentFieldGroupIndex===groupIndex"
:key="groupIndex"
class="form-group flex flex-wrap w-full">
<draggable v-model="currentFields"

View File

@@ -52,11 +52,6 @@
</p>
</template>
</div>
<!-- Field options -->
<!-- <div class="flex-grow" v-if="['files'].includes(field.type) || field.type.startsWith('nf-')">-->
<!-- <pro-tag/>-->
<!-- </div>-->
<template v-if="removing == field.id">
<div class="flex text-sm items-center">

View File

@@ -1,6 +1,6 @@
<template>
<div class="border border-nt-blue-light bg-blue-50 dark:bg-notion-dark-light rounded-md p-4 mb-5 w-full mx-auto mt-4 select-all">
<div v-if="!form.is_pro" class="relative">
<div v-if="false" class="relative">
<div class="absolute inset-0 z-10">
<div class="p-5 max-w-md mx-auto mt-5">
<p class="text-center">
@@ -102,7 +102,7 @@ export default {
},
methods: {
getChartData () {
if (!this.form || !this.form.is_pro) { return null }
if (!this.form) { return null }
this.isLoading = true
axios.get('/api/open/workspaces/' + this.form.workspace_id + '/form-stats/' + this.form.id).then((response) => {
const statsData = response.data

View File

@@ -21,8 +21,13 @@
/>
<toggle-switch-input name="editable_submissions" :form="form" class="mt-4"
label="Allow users to edit their submission"
/>
help="Gives user a unique url to update their submission"
>
<template #label>
Editable submissions
<pro-tag class="ml-1" />
</template>
</toggle-switch-input>
<text-input v-if="form.editable_submissions" name="editable_submissions_button_text"
:form="form"
label="Text of editable submissions button"
@@ -111,7 +116,6 @@
/>
</template>
<template v-else>
<pro-tag class="float-right"/>
<toggle-switch-input name="re_fillable" :form="form" class="mt-4"
label="Allow users to fill the form again"
/>

View File

@@ -12,10 +12,7 @@
</h3>
</template>
<p class="mt-4">
The code will be injected in the <span class="font-semibold">head</span> section of your form page. <a href="#" class="text-gray-500"
@click.prevent="$crisp.push(['do', 'helpdesk:article:open', ['en', 'how-to-inject-custom-code-in-my-form-1amadj3']])"
>Click
here to get an example CSS code.</a>
The code will be injected in the <span class="font-semibold">head</span> section of your form page.
</p>
<code-input name="custom_code" class="mt-4"
:form="form" help="Custom code cannot be previewed in our editor. Please test your code using

View File

@@ -10,7 +10,6 @@
</svg>
Customization
<pro-tag />
</h3>
</template>
@@ -62,9 +61,12 @@
<toggle-switch-input name="hide_title" :form="form" class="mt-4"
label="Hide Title"
/>
<toggle-switch-input name="no_branding" :form="form" class="mt-4"
label="Remove OpnForm Branding"
/>
<toggle-switch-input name="no_branding" :form="form" class="mt-4">
<template #label>
Remove OpnForm Branding
<pro-tag class="ml-1" />
</template>
</toggle-switch-input>
<toggle-switch-input name="uppercase_labels" :form="form" class="mt-4"
label="Uppercase Input Labels"
/>

View File

@@ -17,7 +17,6 @@
label="Protect your form with a Captcha"
help="If enabled we will make sure respondant is a human"
/>
<pro-tag class="float-right" />
<text-input name="password" :form="form" class="mt-4"
label="Form Password" help="Leave empty to disable password"
/>

View File

@@ -2,7 +2,6 @@
<div v-if="logic" :key="resetKey" class="-mx-4 sm:-mx-6 p-5 border-b">
<h3 class="font-semibold block text-lg">
Logic
<pro-tag/>
</h3>
<p class="text-gray-400 text-xs mb-5">
Add some logic to this block. Start by adding some conditions, and then add some actions.

View File

@@ -99,7 +99,6 @@
<div v-if="field.type === 'number'" class="-mx-4 sm:-mx-6 p-5 border-b">
<h3 class="font-semibold block text-lg">
Number Options
<pro-tag/>
</h3>
<v-checkbox v-model="field.is_rating" class="mt-4"
:name="field.id+'_is_rating'" @input="initRating"
@@ -136,7 +135,6 @@
<div v-if="field.type === 'date'" class="-mx-4 sm:-mx-6 p-5 border-b">
<h3 class="font-semibold block text-lg">
Date Options
<pro-tag/>
</h3>
<v-checkbox v-model="field.date_range" class="mt-4"
:name="field.id+'_date_range'"
@@ -191,7 +189,6 @@
<div v-if="['select','multi_select'].includes(field.type)" class="-mx-4 sm:-mx-6 p-5 border-b">
<h3 class="font-semibold block text-lg">
Select Options
<pro-tag/>
</h3>
<p class="text-gray-400 mb-5 text-xs">
Advanced options for your select/multiselect fields.
@@ -218,7 +215,6 @@
<div v-if="displayBasedOnAdvanced" class="-mx-4 sm:-mx-6 p-5 border-b">
<h3 class="font-semibold block text-lg">
Customization
<pro-tag/>
</h3>
<p class="text-gray-400 mb-5 text-xs">
@@ -322,7 +318,6 @@
<div v-if="field.type === 'text'" class="-mx-4 sm:-mx-6 p-5 border-b">
<h3 class="font-semibold block text-lg">
Advanced Options
<pro-tag/>
</h3>
<v-checkbox v-model="field.generates_uuid"

View File

@@ -0,0 +1,107 @@
<template>
<v-transition>
<div v-if="hasCleanings && !hideWarning" class="border border-gray-300 dark:border-gray-600 rounded-md bg-white p-2"
:class="{'hover:bg-yellow-50 dark:hover:bg-yellow-900':!collapseOpened}"
>
<collapse v-model="collapseOpened">
<template #title>
<p class="text-yellow-500 dark:text-yellow-400 font-semibold text-sm p-1 pr-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
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>
</template>
<div class="border-t mt-1 p-4 pb-2 -mx-2">
<p class="text-gray-500 text-sm" v-html="cleaningContent" />
<p class="text-gray-500 text-sm mb-4 font-semibold">
<router-link :to="{name:'pricing'}">
{{ form.is_pro ? 'Upgrade your OpnForms plan today' : 'Start your free OpnForms trial' }}
</router-link>
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>
</collapse>
</div>
</v-transition>
</template>
<script>
import Collapse from '../../../common/Collapse.vue'
import VButton from '../../../common/Button.vue'
import VTransition from '../../../common/transitions/VTransition.vue'
export default {
name: 'FormCleanings',
components: { VTransition, VButton, Collapse },
props: {
form: { type: Object, required: true },
specifyFormOwner: { type: Boolean, default: false },
hideable: { type: Boolean, default: false }
},
data () {
return {
collapseOpened: false,
hideWarning: false
}
},
computed: {
hasCleanings () {
return this.form.cleanings && Object.keys(this.form.cleanings).length > 0
},
cleanings () {
return this.form.cleanings
},
cleaningContent () {
let message = ''
Object.keys(this.cleanings).forEach((key) => {
let fieldName = key.charAt(0).toUpperCase() + key.slice(1)
if (fieldName !== 'Form') {
fieldName = '"' + fieldName + '" field'
}
let fieldInfo = '<span class="font-semibold">' + fieldName + '</span><br/><ul class=\'list-disc list-inside\'>'
this.cleanings[key].forEach((msg) => {
fieldInfo = fieldInfo + '<li>' + msg + '</li>'
})
if (fieldInfo) {
message = message + fieldInfo + '<ul/><br/>'
}
})
return message
}
},
watch: {},
mounted () {
},
methods: {
openCrisp () {
this.$crisp.push(['do', 'chat:show'])
this.$crisp.push(['do', 'chat:open'])
}
}
}
</script>

View File

@@ -13,7 +13,6 @@
/>
</svg>
Url form pre-fill
<pro-tag class="ml-2"/>
</v-button>
<modal :show="showUrlFormPrefillModal" @close="showUrlFormPrefillModal=false">
@@ -26,7 +25,6 @@
</template>
<template #title>
<span>Url Form Prefill</span>
<pro-tag class="ml-4 pb-3" />
</template>
<div class="p-4">

View File

@@ -0,0 +1,94 @@
<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" class="mt-6 block mx-auto"
@click="saveDetails" :arrow="true"
>
Go to checkout
</v-button>
</modal>
</template>
<script>
import { mapGetters } from 'vuex'
import TextInput from '../../forms/TextInput.vue'
import Form from 'vform'
import VButton from '../../common/Button.vue'
import axios from 'axios'
export default {
components: { VButton, TextInput },
props: {
show: {
type: Boolean,
default: false
},
plan: {
type: String,
default: 'pro'
},
yearly: {
type: Boolean,
default: true
}
},
data: () => ({
form: new Form({
name: '',
email: ''
}),
loading: false
}),
watch: {
user () {
this.form.email = this.user.email
},
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 () {
if (this.user) {
this.form.name = this.user.name
this.form.email = this.user.email
}
},
methods: {
saveDetails () {
if (this.form.busy) return
this.form.put('api/subscription/update-customer-details').then(() => {
this.loading = true
axios.get('/api/subscription/new/' + this.plan + '/' + (!this.yearly ? 'monthly' : 'yearly') + '/checkout/with-trial').then((response) => {
window.location = response.data.checkout_url
}).catch((error) => {
this.alertError(error.response.data.message)
}).finally(() => {
this.loading = false
this.close()
})
})
},
close () {
this.$emit('close')
}
},
computed: {
...mapGetters({
user: 'auth/user'
})
}
}
</script>

View File

@@ -0,0 +1,41 @@
<template>
<div class="border border-gray-300 rounded-xl flex p-1 relative">
<button class="font-semibold block flex-grow cursor-pointer">
<div class="p-2 px-3 rounded-lg transition-colors" :class="{'bg-blue-500 text-white':!value}"
@click="set(false)"
>
Monthly
</div>
</button>
<button class="font-semibold block flex-grow cursor-pointer" @click="set(true)">
<div class="p-2 px-4 rounded-lg transition-colors" :class="{'bg-blue-500 text-white':value}">
Yearly
</div>
</button>
<div class="absolute hidden sm:block text-gray-500 text-xs mt-12">
Save 20% with annual plans
</div>
</div>
</template>
<script>
export default {
components: {},
props: {
value: {
type: Boolean,
default: false
}
},
data: () => ({}),
computed: {},
methods: {
set (value) {
this.$emit('input', value)
}
}
}
</script>

View File

@@ -0,0 +1,132 @@
<template>
<div class="w-full">
<section class="relative">
<div class="absolute inset-0 grid" aria-hidden="true">
<div class="bg-gray-100"></div>
<div class="bg-white"></div>
</div>
<div class="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="max-w-5xl mx-auto bg-white shadow-xl rounded-3xl ring-1 ring-gray-200 lg:flex isolate">
<div class="p-8 sm:p-8 lg:flex-auto">
<h3 class="text-3xl font-semibold tracking-tight text-gray-950">
Pro Plan
</h3>
<p class="mt-2 text-base font-medium leading-7 text-gray-600">
OpnForm Pro offers empowering features tailored to the advanced needs of teams and creators. Enjoy our free 3-day trial!
</p>
<div class="flex items-center mt-6 gap-x-4">
<h4 class="flex-none text-sm font-semibold leading-6 tracking-widest text-gray-400 uppercase">
What's included
</h4>
<div class="flex-auto h-px bg-gray-200"></div>
</div>
<ul role="list" class="grid grid-cols-1 gap-4 mt-4 text-sm font-medium leading-6 text-gray-900 sm:grid-cols-2 sm:gap-x-6 sm:gap-y-2">
<li v-for="(title, i) in pricingInfo" :key="i" class="flex gap-x-3">
<svg aria-hidden="true" class="w-5 h-5 shrink-0 stroke-blue-600" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 6L9 17L4 12" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
{{ title }}
</li>
</ul>
</div>
<div class="p-2 -mt-2 lg:mt-0 lg:w-full lg:max-w-md lg:flex-shrink-0">
<div class="py-10 text-center rounded-2xl bg-gray-50 ring-1 ring-inset ring-gray-900/5 lg:flex lg:flex-col lg:justify-center lg:py-12">
<div class="max-w-xs px-8 mx-auto space-y-6">
<div class="flex items-center justify-center mb-10">
<monthly-yearly-selector v-model="isYearly" />
</div><!-- lg+ -->
<p class="flex flex-col items-center">
<span class="text-6xl font-semibold tracking-tight text-gray-950">
<template v-if="isYearly">$16</template>
<template v-else>$19</template>
</span>
<span class="text-sm font-medium leading-6 text-gray-600">
per month
</span>
</p>
<div class="flex justify-center">
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'register' }" :arrow="true">
Start free trial
</v-button>
<v-button v-else-if="authenticated && user.is_subscribed" class="mr-1" @click.prevent="openBilling" :arrow="true">
View Billing
</v-button>
<v-button v-else class="mr-1" @click.prevent="openCustomerCheckout('default')" :arrow="true">
Start free trial
</v-button>
</div>
<p class="text-xs font-medium leading-5 text-gray-600">
Invoices and receipts available for easy company reimbursement.
</p>
</div>
</div>
</div>
</div>
</div>
</section>
<checkout-details-modal :show="showDetailsModal" :yearly="isYearly" :plan="selectedPlan"
@close="showDetailsModal=false"
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import axios from 'axios'
import MonthlyYearlySelector from './MonthlyYearlySelector.vue'
import CheckoutDetailsModal from './CheckoutDetailsModal.vue'
export default {
components: {
MonthlyYearlySelector,
CheckoutDetailsModal,
},
props: {},
data: () => ({
isYearly: true,
selectedPlan: 'pro',
showDetailsModal: false,
pricingInfo: [
'Form confirmation emails',
'Slack notifications',
'Discord notifications',
'Editable submissions',
'Custom domain (soon)',
'Custom code',
'Larger file uploads (50mb)',
'Remove OpnForm branding',
'Priority support'
]
}),
methods: {
openCustomerCheckout (plan) {
this.selectedPlan = plan
this.showDetailsModal = true
},
openBilling () {
this.billingLoading = true
axios.get('/api/subscription/billing-portal').then((response) => {
this.billingLoading = false
const url = response.data.portal_url
window.location = url
})
}
},
computed: {
...mapGetters({
authenticated: 'auth/check',
user: 'auth/user'
})
}
}
</script>