mirror of
https://github.com/fergalmoran/supanextail.git
synced 2025-12-26 11:18:45 +00:00
Check plan (front end) & improve comments
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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'>
|
||||
​
|
||||
</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. We’ve 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>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
1
public/plan.svg
Normal 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 |
@@ -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)",
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user