Skip to content

Commit

Permalink
Pricing UI (#110)
Browse files Browse the repository at this point in the history
* Add initial banner for pricing

* Add new price settings

* Add hiding trial display model

* Add pricing displays for trial period
  • Loading branch information
handotdev authored Jun 29, 2022
1 parent 74f632d commit 505384d
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 32 deletions.
2 changes: 2 additions & 0 deletions server/src/models/Org.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type OrgType = {
name: 'free' | 'pro',
subscribedAt: Date,
customerId: string
isHidingModel: boolean
}
};

Expand Down Expand Up @@ -135,6 +136,7 @@ const OrgSchema = new Schema({
name: { type: String, default: 'free' },
subscribedAt: { type: Date, default: Date.now },
customerId: String,
isHidingModel: Boolean,
}
});

Expand Down
10 changes: 8 additions & 2 deletions server/src/routes/org.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ orgRouter.post('/', async (req, res) => {
users: [user.userId],
});

const redirectUrl = `https://${org.subdomain}.mintlify.com/api/auth/landing?sessionToken=${newSessionToken}`;
const redirectUrl: string = `https://${org.subdomain}.mintlify.com/api/auth/landing?sessionToken=${newSessionToken}`;

track(user.userId, 'Create Organization', {
name: orgName,
Expand Down Expand Up @@ -232,7 +232,13 @@ orgRouter.get('/gitOrg/:gitOrg/details', async (req, res) => {
} catch (error) {
return res.status(400).send({ error });
}

});

orgRouter.delete('/trial/model', userMiddleware, async (_, res) => {
const { org } = res.locals.user;

await Org.findByIdAndUpdate(org._id, {'plan.isHidingModel': true});
res.end();
})

export default orgRouter;
1 change: 0 additions & 1 deletion server/src/routes/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ stripeRouter.get('/checkout', async (req, res) => {
metadata: {
orgId: org._id.toString()
},
trial_period_days: 14
},
});

Expand Down
3 changes: 3 additions & 0 deletions web/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Link from 'next/link'
import ProfilePicture from './ProfilePicture'
import Search from './Search'
import { useProfile } from '../context/ProfileContext'
import TrialBanner from './TrialBanner'

const userNavigation = [
{ name: 'Account', href: '/settings/account' },
Expand All @@ -26,6 +27,7 @@ export default function Navbar() {
useHotkeys('cmd+k', () => setIsSearchOpen(true));

const { user, org } = profile;

if (user == null || org == null) {
return null;
}
Expand All @@ -41,6 +43,7 @@ export default function Navbar() {
<Disclosure as="nav" className="bg-background z-20">
{({ open }) => (
<>
<TrialBanner />
<div className="max-w-7xl mx-auto px-2 sm:px-4 lg:px-8">
<div className="relative flex items-center justify-between h-16 flex-row">
<div className="flex items-center px-2 lg:px-0">
Expand Down
4 changes: 2 additions & 2 deletions web/components/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Fragment, useEffect, useState } from 'react'
import { Combobox, Dialog, Transition } from '@headlessui/react'
import { SearchIcon } from '@heroicons/react/solid'
import { DocumentTextIcon, ExclamationIcon } from '@heroicons/react/outline'
import { ExclamationIcon } from '@heroicons/react/outline'
import { classNames } from '../helpers/functions'
import axios from 'axios'
import { API_ENDPOINT } from '../helpers/api'
Expand Down Expand Up @@ -90,7 +90,7 @@ export default function Search({ isOpen, setIsOpen }: SearchProps) {

return (
<Transition.Root show={isOpen} as={Fragment} appear>
<Dialog as="div" className="relative z-10" onClose={onClose}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
Expand Down
102 changes: 102 additions & 0 deletions web/components/TrialBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { XIcon } from "@heroicons/react/outline";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useProfile } from "../context/ProfileContext";
import { request } from "../helpers/request";

export default function TrialBanner() {
const { profile } = useProfile();
const [isShowingTrialModel, setIsShowingTrialModel] = useState(false);

const { user, org } = profile;

useEffect(() => {
setIsShowingTrialModel(!Boolean(org?.plan?.isHidingModel))
}, [org]);

if (user == null || org == null) {
return null;
}

const onHideTrialAlert = () => {
request('DELETE', 'routes/org/trial/model');
setIsShowingTrialModel(false);
}

const oneDay = 24 * 60 * 60 * 1000;
const dateCreated = Date.parse(org.createdAt);
const now = Date.now();
const daysSinceCreated = Math.round(Math.abs((dateCreated - now) / oneDay));
const trialDays = 14;
const daysLeftOfTrial = trialDays - daysSinceCreated;

if (!isShowingTrialModel && daysLeftOfTrial > 3) {
return null;
}

return <div className="relative bg-primary">
<div className="max-w-7xl mx-auto py-3 px-3 sm:px-6 lg:px-8">
{
daysLeftOfTrial > 3 && <>
<div className="pr-16 sm:text-center sm:px-16">
<p className="font-medium text-white text-sm">
<span className="inline">{daysLeftOfTrial} days left on the trial</span>
<span className="block sm:ml-2 sm:inline-block">
<Link href="/settings/billing">
<a className="text-white font-bold underline">
{' '}
Upgrade now <span aria-hidden="true">&rarr;</span>
</a>
</Link>
</span>
</p>
</div>
<div className="absolute inset-y-0 right-0 pt-1 pr-1 flex items-start sm:pt-1 sm:pr-2 sm:items-start">
<button
type="button"
className="flex p-2 rounded-md hover:bg-hover focus:outline-none focus:ring-2 focus:ring-white"
onClick={onHideTrialAlert}
>
<span className="sr-only">Dismiss</span>
<XIcon className="h-5 w-5 text-white" aria-hidden="true" />
</button>
</div>
</>
}
{
daysLeftOfTrial <= 3 && daysLeftOfTrial > 0 && <>
<div className="pr-16 sm:text-center sm:px-16">
<p className="font-medium text-white text-sm">
<span className="inline">{daysLeftOfTrial} days left on the trial</span>
<span className="block sm:ml-2 sm:inline-block">
<Link href="/settings/billing">
<a className="text-white font-bold underline">
{' '}
Upgrade now <span aria-hidden="true">&rarr;</span>
</a>
</Link>
</span>
</p>
</div>
</>
}
{
daysLeftOfTrial <= 0 && <>
<div className="pr-16 sm:text-center sm:px-16">
<p className="font-medium text-white text-sm">
<span className="inline">The trial period has ended. Upgrade to a plan to continue</span>
<span className="block sm:ml-2 sm:inline-block">
<Link href="/settings/billing">
<a className="text-white font-bold underline">
{' '}
View plans <span aria-hidden="true">&rarr;</span>
</a>
</Link>
</span>
</p>
</div>
</>
}
</div>
</div>
}
4 changes: 3 additions & 1 deletion web/context/ProfileContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export type Org = {
notifications: {
monthlyDigest: boolean
newsletter: boolean
}
},
createdAt: string
access?: {
mode: AccessMode
},
Expand All @@ -38,6 +39,7 @@ export type Org = {
},
plan?: {
name: 'free' | 'pro',
isHidingModel: boolean;
}
}

Expand Down
81 changes: 55 additions & 26 deletions web/pages/settings/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { navigation } from './account'
import { useProfile } from '../../context/ProfileContext'
import { classNames } from '../../helpers/functions'
import { API_ENDPOINT, ISDEV } from '../../helpers/api'
import Link from 'next/link'

type Tier = {
id: string,
Expand All @@ -18,24 +19,13 @@ type Tier = {
yearly: {
price: number,
priceId?: string,
}
},
featuresLabel: string,
description: string,
includedFeatures: string[],
}

const tiers: Tier[] = [
{
id: 'free',
name: 'Free',
monthly: {
price: 0,
},
yearly: {
price: 0,
},
description: 'All the basics for better documentation',
includedFeatures: ['One integration with a documentation platform', 'Track documentation from Notion, Google Docs, and Confluence', 'Workflow automations', 'Max 1 member per organization'],
},
{
id: 'pro',
name: 'Pro',
Expand All @@ -47,17 +37,30 @@ const tiers: Tier[] = [
price: 40,
priceId: ISDEV ? 'price_1LC85UIslOV3ufr23xDxmChm' : 'price_1L7apaIslOV3ufr2k7zGiTds',
},
featuresLabel: 'What\'s included',
description: 'Automating world class documentation',
includedFeatures: [
'Unlimited integrations',
'Track documentation from any platform',
'Private hosted documentation management',
'Unlimited members, teams and ownership assignments',
'Unlimited documents',
'Track documents from any platform',
'Workflow automations',
'Unlimited members and ownership assignments',
'Integrating with task management systems',
'Custom domain',
'Priority on-call support',
'On-call support',
],
},
{
id: 'enterprise',
name: 'Enterprise',
monthly: {
price: -1,
},
yearly: {
price: -1,
},
featuresLabel: 'Includes Pro, plus',
description: 'Built for your enterprises at scale',
includedFeatures: ['SSO and custom authentication', 'Enterprise-grade security and governance', 'Custom domain', 'API access', 'Premium support', 'Tailored onboarding'],
},
]

function PurchaseButton({ tier, currentPlan }: { tier: Tier, currentPlan: string }) {
Expand All @@ -69,14 +72,34 @@ function PurchaseButton({ tier, currentPlan }: { tier: Tier, currentPlan: string
</span>
}

const isFree = tier.id === 'free';
if (tier.id === 'enterprise') {
return <Link href="mailto:[email protected]?subject=Upgrading to Enterprise">
<a
className="mt-8 block w-full bg-gray-700 border border-gray-800 rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-gray-800"
id="checkout-and-portal-button"
target="_blank"
>
Contact us
</a>
</Link>
}

if (tier.id === 'pro' && currentPlan === 'free') {
return <button
className="mt-8 block w-full bg-gray-700 border border-gray-800 rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-gray-800"
id="checkout-and-portal-button"
type="submit"
>
Upgrade now
</button>
}

return <button
className="mt-8 block w-full bg-gray-700 border border-gray-800 rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-gray-800"
id="checkout-and-portal-button"
type="submit"
>
{ isFree ? 'Downgrade' : 'Try for 14 days' }
Downgrade
</button>
}

Expand Down Expand Up @@ -162,8 +185,15 @@ export default function Settings() {
<h2 className="text-lg leading-6 font-medium text-gray-900">{tier.name}</h2>
<p className="mt-4 text-sm text-gray-500">{tier.description}</p>
<p className="mt-8">
<span className="text-4xl font-extrabold text-gray-900">${isMonthly ? tier.monthly.price : tier.yearly.price}</span>{' '}
<span className="text-base font-medium text-gray-500">/mo</span>
{
tier.monthly.price >= 0 && <>
<span className="text-4xl font-medium text-gray-900">${isMonthly ? tier.monthly.price : tier.yearly.price}</span>{' '}
<span className="text-base font-medium text-gray-500">/mo</span>
</>
}
{
tier.monthly.price < 0 && <span className="text-3xl font-medium text-gray-900">Contact us</span>
}
</p>
<form action={`${API_ENDPOINT}/routes/stripe/${tier.id === 'free' ? 'portal' : 'checkout'}`} method="GET">
<input type="hidden" name="priceId" value={isMonthly ? tier.monthly.priceId : tier.yearly.priceId} />
Expand All @@ -176,12 +206,11 @@ export default function Settings() {
</form>
</div>
<div className="pt-6 pb-8 px-6">
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">What&apos;s included</h3>
<h3 className="text-sm font-semibold text-slate-800 tracking-wide uppercase">{tier.featuresLabel}</h3>
<ul role="list" className="mt-6 space-y-4">
{tier.includedFeatures.map((feature) => (
<li key={feature} className="flex space-x-2">
<CheckIcon className="flex-shrink-0 h-5 w-5 text-green-500" aria-hidden="true" />
<span className="text-sm text-gray-500">{feature}</span>
<span className="text-sm text-slate-600">{feature}</span>
</li>
))}
</ul>
Expand Down

1 comment on commit 505384d

@vercel
Copy link

@vercel vercel bot commented on 505384d Jun 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

connect – ./

connect-mintlify.vercel.app
*.mintlify.com
connect-git-main-mintlify.vercel.app

Please sign in to comment.