First stripe integration.

User can subscribe and it will be effective in the database. We now need to reflect this on the front end.
Issue : Need to check how to upgrade a current subscription
This commit is contained in:
Michael
2021-06-24 23:31:41 +02:00
parent 4cdae74c25
commit 92f517cad3
12 changed files with 702 additions and 15 deletions

1
.gitignore vendored
View File

@@ -29,6 +29,7 @@ yarn-error.log*
.env.development.local
.env.test.local
.env.production.local
.env
# vercel
.vercel

View File

@@ -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")
}
}

View File

@@ -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",

View File

@@ -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 (
<div className='w-full mx-auto px-5 py-10 mb-10'>
<div className='text-center max-w-xl mx-auto'>
@@ -88,7 +160,16 @@ const Pricing = () => {
</ul>
</div>
<div className='w-full'>
<button className='btn btn-primary w-full'>Buy Now</button>
<button
className='btn btn-primary w-full'
onClick={(e) =>
handleSubmit(
e,
enabled ? prices.personal.anually : prices.personal.monthly
)
}>
Buy Now
</button>
</div>
</div>
<div className='w-full md:w-1/3 md:max-w-none bg-base-200 px-8 md:px-10 py-8 md:py-10 mb-3 mx-auto md:my-6 rounded-md shadow-lg shadow-gray-600 md:flex md:flex-col'>
@@ -120,7 +201,16 @@ const Pricing = () => {
</ul>
</div>
<div className='w-full'>
<button className='btn btn-primary w-full'>Buy Now</button>
<button
className='btn btn-primary w-full'
onClick={(e) =>
handleSubmit(
e,
enabled ? prices.team.anually : prices.team.monthly
)
}>
Buy Now
</button>
</div>
</div>
<div className='w-full md:w-1/3 md:max-w-none bg-base-200 px-8 md:px-10 py-8 md:py-10 mb-3 mx-auto md:my-6 rounded-md shadow-lg shadow-gray-600 md:flex md:flex-col'>
@@ -152,7 +242,16 @@ const Pricing = () => {
</ul>
</div>
<div className='w-full'>
<button className='btn btn-primary w-full'>Buy Now</button>
<button
className='btn btn-primary w-full'
onClick={(e) =>
handleSubmit(
e,
enabled ? prices.pro.anually : prices.pro.monthly
)
}>
Buy Now
</button>
</div>
</div>
</div>

View File

@@ -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
STRIPE_SECRET=YOUR_SECRET_STRIPE_KEY
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=YOUR_PUBLIC_STRIPE_KEY
STRIPE_WEBHOOK=YOUR_STRIPE_WEBHOOK

309
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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,
},
});
}
}
}

View File

@@ -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 });
}
}

View File

@@ -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);
}
}

13
utils/init-middleware.js Normal file
View File

@@ -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);
});
});
}

14
utils/stripe.js Normal file
View File

@@ -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;