Check plan (front end) & improve comments

This commit is contained in:
Michael
2021-06-25 22:29:55 +02:00
parent 4068f18852
commit 1d9716c2e2
14 changed files with 144 additions and 95 deletions

View File

@@ -5,6 +5,8 @@ function with your new elements.
*/
import Avatar from "./Avatar";
import Image from "next/image";
import Plan from "public/plan.svg";
import { PriceIds } from "utils/priceList";
import { supabase } from "../utils/supabaseClient";
import { toast } from "react-toastify";
@@ -102,9 +104,12 @@ export default function Account(props) {
</div>
</div>
<div className='max-w-xl flex flex-col justify-center m-auto w-full p-5 bordered border-2 border-primary shadow-lg my-5'>
<div className='max-w-xl flex flex-row flex-wrap m-auto w-full p-5 bordered border-2 border-primary shadow-lg my-5'>
<Image src={Plan} />
<div className="flex flex-col m-auto">
<h2>Your current plan</h2>
<p>{props.plan.plan ? PriceIds[props.plan.plan] : "Free tier"}</p>
<p className="font-bold">{props.plan ? PriceIds[props.plan] : "Free tier"}</p>
</div>
</div>
</div>
);

View File

@@ -1,84 +1,77 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'
import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react";
export default function MyModal() {
let [isOpen, setIsOpen] = useState(false)
let [isOpen, setIsOpen] = useState(false);
function closeModal() {
setIsOpen(false)
setIsOpen(false);
}
function openModal() {
setIsOpen(true)
setIsOpen(true);
}
return (
<>
<div className=" inset-0 flex items-center justify-center">
<div className=' inset-0 flex items-center justify-center'>
<button
type="button"
type='button'
onClick={openModal}
className="px-4 py-2 text-sm font-medium text-white bg-black rounded-md bg-opacity-20 hover:bg-opacity-30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
>
className='px-4 py-2 text-sm font-medium text-white bg-black rounded-md bg-opacity-20 hover:bg-opacity-30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75'>
Open dialog
</button>
</div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog
as="div"
className="fixed inset-0 z-10 overflow-y-auto"
onClose={closeModal}
>
<div className="min-h-screen px-4 text-center">
as='div'
className='fixed inset-0 z-10 overflow-y-auto'
onClose={closeModal}>
<div className='min-h-screen px-4 text-center'>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0" />
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-in duration-200'
leaveFrom='opacity-100'
leaveTo='opacity-0'>
<Dialog.Overlay className='fixed inset-0' />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="inline-block h-screen align-middle"
aria-hidden="true"
>
className='inline-block h-screen align-middle'
aria-hidden='true'>
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="inline-block w-full max-w-md p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl">
enter='ease-out duration-300'
enterFrom='opacity-0 scale-95'
enterTo='opacity-100 scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'>
<div className='inline-block w-full max-w-md p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl'>
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
as='h3'
className='text-lg font-medium leading-6 text-gray-900'>
Payment successful
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
<div className='mt-2'>
<p className='text-sm text-gray-500'>
Your payment has been successfully submitted. Weve sent
your an email with all of the details of your order.
</p>
</div>
<div className="mt-4">
<div className='mt-4'>
<button
type="button"
className="inline-flex justify-center px-4 py-2 text-sm font-medium text-blue-900 bg-blue-100 border border-transparent rounded-md hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
onClick={closeModal}
>
type='button'
className='inline-flex justify-center px-4 py-2 text-sm font-medium text-blue-900 bg-blue-100 border border-transparent rounded-md hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500'
onClick={closeModal}>
Got it, thanks!
</button>
</div>
@@ -88,5 +81,5 @@ export default function MyModal() {
</Dialog>
</Transition>
</>
)
);
}

View File

@@ -1,8 +1,12 @@
/*
This is the pricing component.
You can switch between flat payment or subscription by setting the flat variable.
----------
Dont forget to create your customer portal on Stripe
https://dashboard.stripe.com/test/settings/billing/portal
*/
import { getSub, supabase } from "utils/supabaseClient";
import { useEffect, useState } from "react";
import { Auth } from "@supabase/ui";
@@ -10,27 +14,33 @@ import { Prices } from "utils/priceList";
import { Switch } from "@headlessui/react";
import axios from "axios";
import getStripe from "utils/stripe";
import { supabase } from "utils/supabaseClient";
import router from "next/router";
const Pricing = () => {
const [enabled, setEnabled] = useState(false);
const [loading, setLoading] = useState(false);
const { user, session } = Auth.useUser();
const [customerId, setCustomerId] = useState(null);
const [sub, setSub] = useState(false);
const portal = () => {
axios
.post("/api/stripe/customer-portal", {
customerId: customerId,
})
.then((result) => {
router.push(result.data.url);
});
};
useEffect(() => {
if (user) {
getSub().then((result) => setSub(result));
supabase
.from("profiles")
.select(`customerId`)
.eq("id", user.id)
.single()
.then(
(result) =>
// Even if null, it returns something so we check if the customerId is > 1 to be sure it exist
result.data.customerId.length > 1 &&
setCustomerId(result.data.customerId)
);
.then((result) => setCustomerId(result.data?.customerId));
}
}, [user]);
@@ -55,7 +65,6 @@ const Pricing = () => {
const handleSubmit = async (e, priceId) => {
e.preventDefault();
setLoading(true);
// Create a Checkout Session. This will redirect the user to the Stripe website for the payment.
@@ -79,12 +88,6 @@ const Pricing = () => {
// Redirect to Checkout.
const stripe = await getStripe();
// If `redirectToCheckout` fails due to a browser or network
// error, display the localized error message to your customer
// using `error.message`.
setLoading(false);
};
return (
<div className='w-full mx-auto px-5 py-10 mb-10'>
@@ -150,7 +153,12 @@ const Pricing = () => {
<div className='w-full'>
<button
className='btn btn-primary w-full'
onClick={(e) =>
onClick={
sub
? () => {
portal();
}
: (e) =>
handleSubmit(
e,
enabled
@@ -158,7 +166,7 @@ const Pricing = () => {
: Prices.personal.monthly.id
)
}>
Buy Now
{sub ? "Upgrade" : "Buy Now"}
</button>
</div>
</div>
@@ -193,7 +201,12 @@ const Pricing = () => {
<div className='w-full'>
<button
className='btn btn-primary w-full'
onClick={(e) =>
onClick={
sub
? () => {
portal();
}
: (e) =>
handleSubmit(
e,
enabled
@@ -201,7 +214,7 @@ const Pricing = () => {
: Prices.team.monthly.id
)
}>
Buy Now
{sub ? "Upgrade" : "Buy Now"}
</button>
</div>
</div>
@@ -236,15 +249,18 @@ const Pricing = () => {
<div className='w-full'>
<button
className='btn btn-primary w-full'
onClick={(e) =>
onClick={
sub
? () => {
portal();
}
: (e) =>
handleSubmit(
e,
enabled
? Prices.pro.anually.id
: Prices.pro.monthly.id
enabled ? Prices.pro.anually.id : Prices.pro.monthly.id
)
}>
Buy Now
{sub ? "Upgrade" : "Buy Now"}
</button>
</div>
</div>

View File

@@ -1,3 +1,6 @@
/* Dont forget to create your customer portal on Stripe
https://dashboard.stripe.com/test/settings/billing/portal */
import Cors from "cors";
import initMiddleware from "utils/init-middleware";
const rateLimit = require("express-rate-limit");
@@ -25,7 +28,7 @@ export default async function handler(req, res) {
const returnUrl = `${req.headers.origin}/dashboard`;
const portalsession = await stripe.billingPortal.sessions.create({
customer: req.query.customerId,
customer: req.body.customerId,
return_url: returnUrl,
});
res.status(200).send({ url: portalsession.url });

View File

@@ -73,6 +73,7 @@ export default async function handler(req, res) {
.from("subscriptions")
.select("*")
.eq("id", dataObject.client_reference_id);
console.log(dataObject)
if (subscriptions.length == 0) {
const { data, error } = await supabase
@@ -89,6 +90,7 @@ export default async function handler(req, res) {
customer_id: dataObject.customer,
paid_user: true,
plan: dataObject.metadata.priceId,
subscription: dataObject.subscription
},
])
.then()
@@ -100,6 +102,7 @@ export default async function handler(req, res) {
customer_id: dataObject.customer,
paid_user: true,
plan: dataObject.metadata.priceId,
subscription: dataObject.subscription
})
.eq("id", dataObject.client_reference_id)
.then()

View File

@@ -1,3 +1,9 @@
/*
This is the login/register page.
You have 2 components, the "AuthComponent" that handle the logic,
and the "AuthText" that will show the description on the left of the screen
*/
import { Auth } from "@supabase/ui";
import AuthComponent from "../components/Auth";
import AuthText from "components/AuthText";

View File

@@ -1,4 +1,3 @@
import { getSub, supabase } from "../utils/supabaseClient";
import { useEffect, useState } from "react";
import Auth from "../components/Auth";
@@ -6,6 +5,7 @@ import Dashboard from "../components/Dashboard";
import Head from "next/head";
import Layout from "components/Layout";
import { createClient } from "@supabase/supabase-js";
import { supabase } from "../utils/supabaseClient";
import { useRouter } from "next/router";
const DashboardPage = ({ user, plan, profile }) => {
@@ -65,22 +65,34 @@ export async function getServerSideProps({ req }) {
process.env.SUPABASE_ADMIN_KEY
);
const { user } = await supabaseAdmin.auth.api.getUserByCookie(req);
const stripe = require("stripe")(process.env.STRIPE_SECRET);
// If the user exist, you will retrieve the user profile and if he/she's a paid user
if (user) {
let { data: plan, error } = await supabaseAdmin
.from("subscriptions")
.select("paid_user, plan")
.select("subscription")
.eq("id", user.id)
.single();
let { data: profile, errorprofile } = await supabaseAdmin
// Check the subscription plan. If it doesnt exist, return null
const subscription = plan?.subscription
? await stripe.subscriptions.retrieve(plan.subscription)
: null;
let { data: profile, errorProfile } = await supabaseAdmin
.from("profiles")
.select(`username, website, avatar_url`)
.eq("id", user.id)
.single();
return { props: { user, plan, profile } };
return {
props: {
user,
plan: subscription?.plan?.id ? subscription.plan.id : null,
profile,
},
};
}
if (!user) {

View File

@@ -1,4 +1,7 @@
/*Don't forget to modify the Head component with your website informations */
/*
Don't forget to modify the Head component with your website informations
You can also update the content on the Landing.js component
*/
import Head from "next/head";
import Landing from "components/Landing";

View File

@@ -1,3 +1,5 @@
// To modify the content of the pricing page, check the Pricing.js component
import Layout from "components/Layout";
import { NextSeo } from "next-seo";
import Pricing from "components/Pricing";

View File

@@ -1,3 +1,5 @@
// To modify the privacy policy, check the PrivacyPolicy.js component
import Layout from "components/Layout";
import { NextSeo } from "next-seo";
import PrivacyPolicy from "components/PrivacyPolicy";

View File

@@ -1,3 +1,5 @@
// To modify the terms & conditions, check the Terms.js component
import Layout from "components/Layout";
import { NextSeo } from "next-seo";
import Terms from "components/Terms";

1
public/plan.svg Normal file
View File

@@ -0,0 +1 @@
<svg viewBox="0 0 150 150" height="150" width="150" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.5,0,0,1.5,0,0)"><path d="M10.000 50.000 A40.000 40.000 0 1 0 90.000 50.000 A40.000 40.000 0 1 0 10.000 50.000 Z" fill="#e8f4fa" stroke="#daedf7"></path><path d="M33.750 82.500 A16 1.5 0 1 0 65.750 82.500 A16 1.5 0 1 0 33.750 82.500 Z" fill="#525252" opacity=".15"></path><path d="M35.294,38.8H64.751a3.978,3.978,0,0,1,3.978,3.978V58.819A3.978,3.978,0,0,1,64.751,62.8H35.293a3.977,3.977,0,0,1-3.977-3.977V42.774A3.978,3.978,0,0,1,35.294,38.8Z" fill="#ebcb00"></path><path d="M49.555,38.8H35.293a3.978,3.978,0,0,0-3.976,3.978v14.26Z" fill="#ffe500"></path><path d="M36.258 57.149L43.317 57.149" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round"></path><path d="M36.258 47.267 L43.317 47.267 L43.317 52.208 L36.258 52.208 Z" fill="#ffef9e" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round"></path><path d="M35.294,38.8H64.751a3.978,3.978,0,0,1,3.978,3.978V58.819A3.978,3.978,0,0,1,64.751,62.8H35.293a3.977,3.977,0,0,1-3.977-3.977V42.774A3.978,3.978,0,0,1,35.294,38.8Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round"></path><path d="M51.787 55.031 A4.235 4.235 0 1 0 60.257 55.031 A4.235 4.235 0 1 0 51.787 55.031 Z" fill="#ff6242"></path><path d="M56.729 55.031 A4.235 4.235 0 1 0 65.199 55.031 A4.235 4.235 0 1 0 56.729 55.031 Z" fill="#ffef9e"></path><path d="M56.728,55.031a4.23,4.23,0,0,0,1.765,3.439,4.233,4.233,0,0,0,0-6.878A4.231,4.231,0,0,0,56.728,55.031Z" fill="#ffaa54"></path><path d="M51.787 55.031 A4.235 4.235 0 1 0 60.257 55.031 A4.235 4.235 0 1 0 51.787 55.031 Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round"></path><path d="M56.729 55.031 A4.235 4.235 0 1 0 65.199 55.031 A4.235 4.235 0 1 0 56.729 55.031 Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round"></path><path d="M61.739,21.38a1.264,1.264,0,0,1-.708-1.627l.779-2.024a1,1,0,0,0-1.36-1.264L51.079,20.9a1,1,0,0,0-.46,1.364l4.393,8.5a1,1,0,0,0,1.822-.1l.823-2.14a1.13,1.13,0,0,1,1.473-.641,24.933,24.933,0,1,1-18.846.587,2,2,0,0,0,.948-2.784l-1.427-2.624a2,2,0,0,0-2.566-.873A31.305,31.305,0,1,0,81.5,50.619,31.362,31.362,0,0,0,61.739,21.38Z" fill="#9ceb60"></path><path d="M18.56,48.845a31.495,31.495,0,0,0,62.885-.1q.054.931.054,1.875a31.5,31.5,0,1,1-63,0C18.5,50.053,18.519,49.461,18.56,48.845Z" fill="#6dd627"></path><path d="M61.739,21.38a1.264,1.264,0,0,1-.708-1.627l.779-2.024a1,1,0,0,0-1.36-1.264L51.079,20.9a1,1,0,0,0-.46,1.364l4.393,8.5a1,1,0,0,0,1.822-.1l.823-2.14a1.13,1.13,0,0,1,1.473-.641,24.933,24.933,0,1,1-18.846.587,2,2,0,0,0,.948-2.784l-1.427-2.624a2,2,0,0,0-2.566-.873A31.305,31.305,0,1,0,81.5,50.619,31.362,31.362,0,0,0,61.739,21.38Z" fill="none" stroke="#45413c" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -23,7 +23,7 @@ const Prices = {
},
pro: {
monthly: {
id: "price_1J5q3TDMjD0UnVmMJKX3nkDq",
id: "price_1J6KRuDMjD0UnVmMIItaOdT3",
desc: "Pro plan (monthly)",
},
anually: {
@@ -38,7 +38,7 @@ const PriceIds = {
price_1J5q45DMjD0UnVmMQxXHKGAv: "Personal plan (anually)",
price_1J5q3GDMjD0UnVmMlHc5Eedq: "Team plan (monthly)",
price_1J5q8zDMjD0UnVmMqsngM91X: "Team plan (anually)",
price_1J5q3TDMjD0UnVmMJKX3nkDq: "Pro plan (monthly)",
price_1J6KRuDMjD0UnVmMIItaOdT3: "Pro plan (monthly)",
price_1J5q9VDMjD0UnVmMIQtVDSZ9: "Pro plan (anually)",
};

View File

@@ -9,7 +9,8 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey);
export const getSub = async () => {
let { data: subscriptions, error } = await supabase
.from("subscriptions")
.select("paid_user, plan");
.select("paid_user, plan")
.single();
if (subscriptions) {
return subscriptions;
}