diff --git a/.gitignore b/.gitignore
index 1437c53..c9cabe4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,7 @@ yarn-error.log*
.env.development.local
.env.test.local
.env.production.local
+.env
# vercel
-.vercel
+.vercel
\ No newline at end of file
diff --git a/components/Dashboard.js b/components/Dashboard.js
index cdcd61a..0acebbb 100644
--- a/components/Dashboard.js
+++ b/components/Dashboard.js
@@ -7,8 +7,8 @@ function with your new elements.
import { useEffect, useState } from "react";
import Avatar from "./Avatar";
-import MyModal from "./MyModal";
import { supabase } from "../utils/supabaseClient";
+import { toast } from "react-toastify";
export default function Account({ session }) {
const [loading, setLoading] = useState(true);
@@ -71,6 +71,7 @@ export default function Account({ session }) {
alert(error.message);
} finally {
setLoading(false);
+ toast.success("Your profile has been updated")
}
}
diff --git a/components/Layout.js b/components/Layout.js
index 116aeae..7b8c4fd 100644
--- a/components/Layout.js
+++ b/components/Layout.js
@@ -23,7 +23,7 @@ const Layout = (props) => {
const toastStyle = {
//Style your toast elements here
- success: "bg-secondary",
+ success: "bg-accent",
error: "bg-red-600",
info: "bg-gray-600",
warning: "bg-orange-400",
diff --git a/components/Pricing.js b/components/Pricing.js
index 56e19e4..04762f2 100644
--- a/components/Pricing.js
+++ b/components/Pricing.js
@@ -3,12 +3,50 @@ This is the pricing component.
You can switch between flat payment or subscription by setting the flat variable.
*/
+import { useEffect, useState } from "react";
+
+import { Auth } from "@supabase/ui";
import { Switch } from "@headlessui/react";
-import { useState } from "react";
+import axios from "axios";
+import getStripe from "utils/stripe";
+import { supabase } from "utils/supabaseClient";
const Pricing = () => {
const [enabled, setEnabled] = useState(false);
- const flat = false;
+ const [loading, setLoading] = useState(false);
+ const { user, session } = Auth.useUser();
+ const [customerId, setCustomerId] = useState(null);
+
+ useEffect(() => {
+ if (user) {
+ 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)
+ );
+ }
+ }, [user]);
+ const prices = {
+ personal: {
+ monthly: "price_1J5q2yDMjD0UnVmMXzEWYDnl",
+ anually: "price_1J5q45DMjD0UnVmMQxXHKGAv",
+ },
+ team: {
+ monthly: "price_1J5q3GDMjD0UnVmMlHc5Eedq",
+ anually: "price_1J5q8zDMjD0UnVmMqsngM91X",
+ },
+ pro: {
+ monthly: "price_1J5q3TDMjD0UnVmMJKX3nkDq",
+ anually: "price_1J5q9VDMjD0UnVmMIQtVDSZ9",
+ },
+ };
+ const flat = false; // Switch between subscription system or flat prices
const pricing = {
monthly: {
personal: "$5/mo",
@@ -26,6 +64,40 @@ const Pricing = () => {
pro: "€149",
},
};
+
+ 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.
+
+ axios
+ .post("/api/stripe/create-checkout-session", {
+ priceId: priceId,
+ email: user.email,
+ customerId: customerId,
+ userId: user.id,
+ tokenId: session.access_token,
+ })
+ .then((result) => {
+ stripe
+ .redirectToCheckout({
+ sessionId: result.data.sessionId,
+ })
+ .then((result) => {
+ console.log(result);
+ });
+ });
+
+ // 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 (
@@ -88,7 +160,16 @@ const Pricing = () => {
-
+
@@ -120,7 +201,16 @@ const Pricing = () => {
-
+
@@ -152,7 +242,16 @@ const Pricing = () => {
-
+
diff --git a/env.local.example b/env.local.example
index a3f78cc..a72c36f 100644
--- a/env.local.example
+++ b/env.local.example
@@ -1,6 +1,10 @@
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
+SUPABASE_ADMIN_KEY=YOUR_SUPABASE_ADMIN_KEY
NEXT_PUBLIC_TITLE="SupaNexTail"
SENDGRID_SECRET=YOUR_SENGRID_SECRET
SENDGRID_MAILTO=YOUR_RECIPIENT
-SENDGRID_MAILFROM=YOUR_VERIFIED_SENDER
\ No newline at end of file
+SENDGRID_MAILFROM=YOUR_VERIFIED_SENDER
+STRIPE_SECRET=YOUR_SECRET_STRIPE_KEY
+NEXT_PUBLIC_STRIPE_PUBLIC_KEY=YOUR_PUBLIC_STRIPE_KEY
+STRIPE_WEBHOOK=YOUR_STRIPE_WEBHOOK
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 7d77b27..ab86d00 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,26 +1,30 @@
{
- "name": "with-tailwindcss",
- "version": "0.1.0",
+ "name": "supanextail",
+ "version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "with-tailwindcss",
- "version": "0.1.0",
+ "version": "0.4.0",
"dependencies": {
"@headlessui/react": "^1.2.0",
"@sendgrid/mail": "^7.4.4",
+ "@stripe/stripe-js": "^1.15.1",
"@supabase/supabase-js": "^1.15.0",
"@supabase/ui": "^0.27.3",
"axios": "^0.21.1",
+ "cors": "^2.8.5",
"daisyui": "^1.3.4",
+ "express-rate-limit": "^5.2.6",
+ "micro": "^9.3.4",
"next": "^11.0.0",
"next-seo": "^4.24.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-feather": "^2.0.9",
"react-icons": "^4.2.0",
- "react-toastify": "^7.0.4"
+ "react-toastify": "^7.0.4",
+ "stripe": "^8.156.0"
},
"devDependencies": {
"autoprefixer": "^10.0.4",
@@ -300,6 +304,11 @@
"node": "6.* || 8.* || >=10.*"
}
},
+ "node_modules/@stripe/stripe-js": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.15.1.tgz",
+ "integrity": "sha512-yJiDGutlwu25iajCy51VRJeoH3UMs+s5qVIDGfmPUuFpZ+F6AJ9g9EFrsBNvHxAGBahQFMLlBdzlCVydhGp6tg=="
+ },
"node_modules/@supabase/gotrue-js": {
"version": "1.16.5",
"resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.16.5.tgz",
@@ -895,6 +904,14 @@
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
"integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U="
},
+ "node_modules/content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/convert-source-map": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
@@ -913,6 +930,18 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/cosmiconfig": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz",
@@ -1346,6 +1375,11 @@
"safe-buffer": "^5.1.1"
}
},
+ "node_modules/express-rate-limit": {
+ "version": "5.2.6",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.6.tgz",
+ "integrity": "sha512-nE96xaxGfxiS5jP3tD3kIW1Jg9yQgX0rXCs3rCkZtmbWHEGyotwaezkLj7bnB41Z0uaOLM8W4AX6qHao4IZ2YA=="
+ },
"node_modules/ext": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
@@ -1953,6 +1987,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-string": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz",
@@ -2185,6 +2227,90 @@
"node": ">= 8"
}
},
+ "node_modules/micro": {
+ "version": "9.3.4",
+ "resolved": "https://registry.npmjs.org/micro/-/micro-9.3.4.tgz",
+ "integrity": "sha512-smz9naZwTG7qaFnEZ2vn248YZq9XR+XoOH3auieZbkhDL4xLOxiE+KqG8qqnBeKfXA9c1uEFGCxPN1D+nT6N7w==",
+ "dependencies": {
+ "arg": "4.1.0",
+ "content-type": "1.0.4",
+ "is-stream": "1.1.0",
+ "raw-body": "2.3.2"
+ },
+ "bin": {
+ "micro": "bin/micro.js"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/micro/node_modules/arg": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz",
+ "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="
+ },
+ "node_modules/micro/node_modules/bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/micro/node_modules/depd": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
+ "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micro/node_modules/http-errors": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
+ "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
+ "dependencies": {
+ "depd": "1.1.1",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.0.3",
+ "statuses": ">= 1.3.1 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micro/node_modules/iconv-lite": {
+ "version": "0.4.19",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
+ "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/micro/node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "node_modules/micro/node_modules/raw-body": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
+ "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
+ "dependencies": {
+ "bytes": "3.0.0",
+ "http-errors": "1.6.2",
+ "iconv-lite": "0.4.19",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/micro/node_modules/setprototypeof": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
+ "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
+ },
"node_modules/micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
@@ -3034,6 +3160,20 @@
"purgecss": "bin/purgecss.js"
}
},
+ "node_modules/qs": {
+ "version": "6.10.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz",
+ "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/querystring": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz",
@@ -3393,6 +3533,19 @@
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
},
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@@ -3522,6 +3675,18 @@
"node": ">=8"
}
},
+ "node_modules/stripe": {
+ "version": "8.156.0",
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.156.0.tgz",
+ "integrity": "sha512-q+bixlhaxnSI/Htk/iB1i5LhuZ557hL0pFgECBxQNhso1elxIsOsPOIXEuo3tSLJEb8CJSB7t/+Fyq6KP69tAQ==",
+ "dependencies": {
+ "@types/node": ">=8.1.0",
+ "qs": "^6.6.0"
+ },
+ "engines": {
+ "node": "^8.1 || >=10.*"
+ }
+ },
"node_modules/styled-jsx": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-3.3.2.tgz",
@@ -3905,6 +4070,14 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/vm-browserify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
@@ -4241,6 +4414,11 @@
"@sendgrid/helpers": "^7.4.3"
}
},
+ "@stripe/stripe-js": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.15.1.tgz",
+ "integrity": "sha512-yJiDGutlwu25iajCy51VRJeoH3UMs+s5qVIDGfmPUuFpZ+F6AJ9g9EFrsBNvHxAGBahQFMLlBdzlCVydhGp6tg=="
+ },
"@supabase/gotrue-js": {
"version": "1.16.5",
"resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.16.5.tgz",
@@ -4730,6 +4908,11 @@
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
"integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U="
},
+ "content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+ },
"convert-source-map": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
@@ -4750,6 +4933,15 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
+ "cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "requires": {
+ "object-assign": "^4",
+ "vary": "^1"
+ }
+ },
"cosmiconfig": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz",
@@ -5117,6 +5309,11 @@
"safe-buffer": "^5.1.1"
}
},
+ "express-rate-limit": {
+ "version": "5.2.6",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.6.tgz",
+ "integrity": "sha512-nE96xaxGfxiS5jP3tD3kIW1Jg9yQgX0rXCs3rCkZtmbWHEGyotwaezkLj7bnB41Z0uaOLM8W4AX6qHao4IZ2YA=="
+ },
"ext": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
@@ -5533,6 +5730,11 @@
"has-symbols": "^1.0.2"
}
},
+ "is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
+ },
"is-string": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz",
@@ -5711,6 +5913,71 @@
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true
},
+ "micro": {
+ "version": "9.3.4",
+ "resolved": "https://registry.npmjs.org/micro/-/micro-9.3.4.tgz",
+ "integrity": "sha512-smz9naZwTG7qaFnEZ2vn248YZq9XR+XoOH3auieZbkhDL4xLOxiE+KqG8qqnBeKfXA9c1uEFGCxPN1D+nT6N7w==",
+ "requires": {
+ "arg": "4.1.0",
+ "content-type": "1.0.4",
+ "is-stream": "1.1.0",
+ "raw-body": "2.3.2"
+ },
+ "dependencies": {
+ "arg": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz",
+ "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="
+ },
+ "bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
+ },
+ "depd": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
+ "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
+ },
+ "http-errors": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
+ "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
+ "requires": {
+ "depd": "1.1.1",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.0.3",
+ "statuses": ">= 1.3.1 < 2"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.19",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
+ "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "raw-body": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
+ "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
+ "requires": {
+ "bytes": "3.0.0",
+ "http-errors": "1.6.2",
+ "iconv-lite": "0.4.19",
+ "unpipe": "1.0.0"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
+ "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
+ }
+ }
+ },
"micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
@@ -6371,6 +6638,14 @@
"postcss-selector-parser": "^6.0.2"
}
},
+ "qs": {
+ "version": "6.10.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz",
+ "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==",
+ "requires": {
+ "side-channel": "^1.0.4"
+ }
+ },
"querystring": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz",
@@ -6628,6 +6903,16 @@
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
},
+ "side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "requires": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ }
+ },
"simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@@ -6738,6 +7023,15 @@
"ansi-regex": "^5.0.0"
}
},
+ "stripe": {
+ "version": "8.156.0",
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.156.0.tgz",
+ "integrity": "sha512-q+bixlhaxnSI/Htk/iB1i5LhuZ557hL0pFgECBxQNhso1elxIsOsPOIXEuo3tSLJEb8CJSB7t/+Fyq6KP69tAQ==",
+ "requires": {
+ "@types/node": ">=8.1.0",
+ "qs": "^6.6.0"
+ }
+ },
"styled-jsx": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-3.3.2.tgz",
@@ -7038,6 +7332,11 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+ },
"vm-browserify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
diff --git a/package.json b/package.json
index 5cad476..71a0c49 100644
--- a/package.json
+++ b/package.json
@@ -10,17 +10,22 @@
"dependencies": {
"@headlessui/react": "^1.2.0",
"@sendgrid/mail": "^7.4.4",
+ "@stripe/stripe-js": "^1.15.1",
"@supabase/supabase-js": "^1.15.0",
"@supabase/ui": "^0.27.3",
"axios": "^0.21.1",
+ "cors": "^2.8.5",
"daisyui": "^1.3.4",
+ "express-rate-limit": "^5.2.6",
+ "micro": "^9.3.4",
"next": "^11.0.0",
"next-seo": "^4.24.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-feather": "^2.0.9",
"react-icons": "^4.2.0",
- "react-toastify": "^7.0.4"
+ "react-toastify": "^7.0.4",
+ "stripe": "^8.156.0"
},
"devDependencies": {
"autoprefixer": "^10.0.4",
diff --git a/pages/api/stripe/create-checkout-session.js b/pages/api/stripe/create-checkout-session.js
new file mode 100644
index 0000000..70d6981
--- /dev/null
+++ b/pages/api/stripe/create-checkout-session.js
@@ -0,0 +1,83 @@
+import Cors from "cors";
+import initMiddleware from "utils/init-middleware";
+
+const rateLimit = require("express-rate-limit");
+
+const cors = initMiddleware(
+ Cors({
+ methods: ["POST"],
+ })
+);
+
+const limiter = initMiddleware(
+ rateLimit({
+ windowMs: 30000, // 30sec
+ max: 4, // Max 4 request per 30 sec
+ })
+);
+// Set your secret key. Remember to switch to your live secret key in production.
+// See your keys here: https://dashboard.stripe.com/apikeys
+const stripe = require("stripe")(process.env.STRIPE_SECRET);
+
+export default async function handler(req, res) {
+ await cors(req, res);
+ await limiter(req, res);
+ if (req.method === "POST") {
+ const priceId = req.body.priceId;
+
+ // See https://stripe.com/docs/api/checkout/sessions/create
+ // for additional parameters to pass.
+ try {
+ const session = req.body.customerId
+ ? await stripe.checkout.sessions.create({
+ mode: "subscription",
+ payment_method_types: ["card"],
+ client_reference_id: req.body.userId,
+ metadata: { token: req.body.tokenId, priceId: req.body.priceId },
+ customer: req.body.customerId,
+ line_items: [
+ {
+ price: priceId,
+ // For metered billing, do not pass quantity
+ quantity: 1,
+ },
+ ],
+ // {CHECKOUT_SESSION_ID} is a string literal; do not change it!
+ // the actual Session ID is returned in the query parameter when your customer
+ // is redirected to the success page.
+ success_url: `${req.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
+ cancel_url: `${req.headers.origin}/dashboard?session_id=canceled`,
+ })
+ : await stripe.checkout.sessions.create({
+ mode: "subscription",
+ payment_method_types: ["card"],
+ customer_email: req.body.email,
+ client_reference_id: req.body.userId,
+ metadata: { token: req.body.tokenId, priceId: req.body.priceId },
+ line_items: [
+ {
+ price: priceId,
+ // For metered billing, do not pass quantity
+ quantity: 1,
+ },
+ ],
+ // {CHECKOUT_SESSION_ID} is a string literal; do not change it!
+ // the actual Session ID is returned in the query parameter when your customer
+ // is redirected to the success page.
+ success_url: `${req.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
+ cancel_url: `${req.headers.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
+ });
+
+ res.send({
+ sessionId: session.id,
+ });
+ } catch (e) {
+ res.status(400);
+ return res.send({
+ error: {
+ message: e.message,
+ },
+ });
+ }
+ }
+}
diff --git a/pages/api/stripe/customer-portal.js b/pages/api/stripe/customer-portal.js
new file mode 100644
index 0000000..dc3e591
--- /dev/null
+++ b/pages/api/stripe/customer-portal.js
@@ -0,0 +1,33 @@
+import Cors from "cors";
+import initMiddleware from "utils/init-middleware";
+const rateLimit = require("express-rate-limit");
+
+const cors = initMiddleware(
+ Cors({
+ methods: ["POST", "PUT"],
+ })
+);
+
+const limiter = initMiddleware(
+ rateLimit({
+ windowMs: 30000, // 30sec
+ max: 150, // Max 4 request per 30 sec
+ })
+);
+// Set your secret key. Remember to switch to your live secret key in production.
+// See your keys here: https://dashboard.stripe.com/apikeys
+const stripe = require("stripe")(process.env.STRIPE_SECRET);
+
+export default async function handler(req, res) {
+ await cors(req, res);
+ await limiter(req, res);
+ if (req.method === "POST") {
+ const returnUrl = `${req.headers.origin}/dashboard`;
+
+ const portalsession = await stripe.billingPortal.sessions.create({
+ customer: req.query.customerId,
+ return_url: returnUrl,
+ });
+ res.status(200).send({ url: portalsession.url });
+ }
+}
diff --git a/pages/api/stripe/stripe-webhook.js b/pages/api/stripe/stripe-webhook.js
new file mode 100644
index 0000000..633eff5
--- /dev/null
+++ b/pages/api/stripe/stripe-webhook.js
@@ -0,0 +1,135 @@
+import Cors from "cors";
+import { buffer } from "micro";
+import { createClient } from "@supabase/supabase-js";
+import initMiddleware from "utils/init-middleware";
+const rateLimit = require("express-rate-limit");
+
+export const config = {
+ api: {
+ bodyParser: false,
+ },
+};
+
+// Initialize the cors middleware -> Allow the browser extension to create lists
+const cors = initMiddleware(
+ Cors({
+ methods: ["POST", "HEAD"],
+ })
+);
+
+// Init Supabase Admin
+
+const supabase = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL,
+ process.env.SUPABASE_ADMIN_KEY
+);
+
+// Rate limiter : The user can only create one list every 20 seconds (avoid spam)
+
+const limiter = initMiddleware(
+ rateLimit({
+ windowMs: 30000, // 30sec
+ max: 150, // Max 150 request per 30 sec
+ })
+);
+// Set your secret key. Remember to switch to your live secret key in production.
+// See your keys here: https://dashboard.stripe.com/apikeys
+const stripe = require("stripe")(process.env.STRIPE_SECRET);
+
+export default async function handler(req, res) {
+ await cors(req, res);
+ await limiter(req, res);
+ stripe.setMaxNetworkRetries(2);
+
+ if (req.method === "POST") {
+ // Retrieve the event by verifying the signature using the raw body and secret.
+ let event;
+ const buf = await buffer(req);
+
+ try {
+ event = stripe.webhooks.constructEvent(
+ buf,
+ req.headers["stripe-signature"],
+ process.env.STRIPE_WEBHOOK
+ );
+ } catch (err) {
+ console.log(err);
+ console.log(`⚠️ Webhook signature verification failed.`);
+ console.log(
+ `⚠️ Check the env file and enter the correct webhook secret.`
+ );
+ return res.send(400);
+ }
+ // Extract the object from the event.
+ const dataObject = event.data.object;
+
+ // Handle the event
+ // Review important events for Billing webhooks
+ // https://stripe.com/docs/billing/webhooks
+ // Remove comment to see the various objects sent for this sample
+ switch (event.type) {
+ case "checkout.session.completed":
+ let { data: subscriptions, error } = await supabase
+ .from("subscriptions")
+ .select("*")
+ .eq("id", dataObject.client_reference_id);
+
+ if (subscriptions.length == 0) {
+ const { data, error } = await supabase
+ .from("profiles")
+ .update({ customerId: dataObject.customer })
+ .eq("id", dataObject.client_reference_id);
+ if (error) console.log(error);
+
+ await supabase
+ .from("subscriptions")
+ .insert([
+ {
+ id: dataObject.client_reference_id,
+ customer_id: dataObject.customer,
+ paid_user: true,
+ plan: dataObject.metadata.priceId,
+ },
+ ])
+ .then()
+ .catch((err) => console.log(err));
+ } else if (subscriptions.length > 0) {
+ await supabase
+ .from("subscriptions")
+ .update({
+ customer_id: dataObject.customer,
+ paid_user: true,
+ plan: dataObject.metadata.priceId,
+ })
+ .eq("id", dataObject.client_reference_id)
+ .then()
+ .catch((err) => console.log(err));
+ }
+ break;
+ case "customer.subscription.deleted":
+ await supabase
+ .from("subscriptions")
+ .update({ paid_user: false })
+ .eq("customer_id", dataObject.customer)
+ .then()
+ .catch((err) => console.log(err));
+ break;
+ case "invoice.payment_failed":
+ // If the payment fails or the customer does not have a valid payment method,
+ // an invoice.payment_failed event is sent, the subscription becomes past_due.
+ // Use this webhook to notify your user that their payment has
+ // failed and to retrieve new card details.
+ break;
+
+ case "invoice.paid":
+ // Used to provision services after the trial has ended.
+ // The status of the invoice will show up as paid. Store the status in your
+ // database to reference when a user accesses your service to avoid hitting rate limits.
+ break;
+
+ default:
+ // Unexpected event type
+ }
+ res.send(200);
+ }
+}
diff --git a/utils/init-middleware.js b/utils/init-middleware.js
new file mode 100644
index 0000000..a70041d
--- /dev/null
+++ b/utils/init-middleware.js
@@ -0,0 +1,13 @@
+// Helper method to wait for a middleware to execute before continuing
+// And to throw an error when an error happens in a middleware
+export default function initMiddleware(middleware) {
+ return (req, res) =>
+ new Promise((resolve, reject) => {
+ middleware(req, res, (result) => {
+ if (result instanceof Error) {
+ return reject(result);
+ }
+ return resolve(result);
+ });
+ });
+}
diff --git a/utils/stripe.js b/utils/stripe.js
new file mode 100644
index 0000000..008c467
--- /dev/null
+++ b/utils/stripe.js
@@ -0,0 +1,14 @@
+/**
+ * This is a singleton to ensure we only instantiate Stripe once.
+ */
+import { loadStripe } from "@stripe/stripe-js";
+
+let stripePromise = null;
+const getStripe = () => {
+ if (!stripePromise) {
+ stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY);
+ }
+ return stripePromise;
+};
+
+export default getStripe;