add docs
3
.env.client.example
Normal file
@@ -0,0 +1,3 @@
|
||||
# learn more about client side env vars https://wasp-lang.dev/docs/project/env-vars
|
||||
|
||||
REACT_APP_SOME_VAR_NAME=foo
|
@@ -3,11 +3,14 @@
|
||||
# If you use `wasp start db` then you DO NOT need to add a DATABASE_URL env variable here.
|
||||
# DATABASE_URL=
|
||||
|
||||
# TODO: replace the comments with links to the SaaS Template docs
|
||||
|
||||
# for testing, go to https://dashboard.stripe.com/test/apikeys and get a test stripe key that starts with "sk_test_..."
|
||||
STRIPE_KEY=
|
||||
STRIPE_KEY=sk_test_...
|
||||
# to create a test subscription, go to https://dashboard.stripe.com/test/products and click on + Add Product
|
||||
SUBSCRIPTION_PRICE_ID=
|
||||
# after starting the stripe cli with `stripe listen --forward-to localhost:3001/stripe-webhook` it will output your signing secret
|
||||
HOBBY_SUBSCRIPTION_PRICE_ID=price_...
|
||||
PRO_SUBSCRIPTION_PRICE_ID=price_...
|
||||
# after downloading starting the stripe cli (https://stripe.com/docs/stripe-cli) with `stripe listen --forward-to localhost:3001/stripe-webhook` it will output your signing secret
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
|
||||
# see our guide for setting up google auth: https://wasp-lang.dev/docs/auth/social-auth/google
|
||||
|
21
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
4
docs/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
11
docs/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
53
docs/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Starlight Starter Kit: Basics
|
||||
|
||||
[](https://starlight.astro.build)
|
||||
|
||||
```
|
||||
npm create astro@latest -- --template starlight
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
|
||||
[](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
.
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── assets/
|
||||
│ ├── content/
|
||||
│ │ ├── docs/
|
||||
│ │ └── config.ts
|
||||
│ └── env.d.ts
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
|
||||
|
||||
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
|
||||
|
||||
Static assets, like favicons, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
|
61
docs/astro.config.mjs
Normal file
@@ -0,0 +1,61 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
import starlightBlog from 'starlight-blog'
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
starlightBlog({
|
||||
title: 'The Best Blog Ever',
|
||||
authors: {
|
||||
vince: {
|
||||
name: 'Vince',
|
||||
title: 'Dev Rel @ Wasp',
|
||||
picture: '/CRAIG_ROCK.png', // Images in the `public` directory are supported.
|
||||
url: 'https://wasp-lang.dev',
|
||||
},
|
||||
},
|
||||
}),
|
||||
starlight({
|
||||
title: 'My Docs',
|
||||
components: {
|
||||
MarkdownContent: 'starlight-blog/overrides/MarkdownContent.astro',
|
||||
Sidebar: 'starlight-blog/overrides/Sidebar.astro',
|
||||
ThemeSelect: 'starlight-blog/overrides/ThemeSelect.astro',
|
||||
},
|
||||
social: {
|
||||
github: 'https://github.com/withastro/starlight',
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Start Here',
|
||||
items: [
|
||||
{ label: 'Introduction', link: '/start/introduction/' },
|
||||
{ label: 'Getting Started', link: '/start/getting-started/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Guides',
|
||||
items: [
|
||||
{
|
||||
label: 'Auth ',
|
||||
link: '/guides/auth/',
|
||||
items: [
|
||||
{ label: 'Turtle', link: '/guides/components/' },
|
||||
{ label: 'Internationalization (i18n)', link: '/guides/i18n/' },
|
||||
],
|
||||
},
|
||||
{ label: 'Stripe Integration', link: '/guides/stripe-integration/' },
|
||||
{ label: 'Stripe Testing', link: '/guides/stripe-testing/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'General',
|
||||
items: [
|
||||
{ label: 'User Permissions', link: 'general/user-permissions/' },
|
||||
],
|
||||
}
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
12731
docs/package-lock.json
generated
Normal file
20
docs/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro check && astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.3.1",
|
||||
"@astrojs/starlight": "^0.13.0",
|
||||
"astro": "^3.2.3",
|
||||
"sharp": "^0.32.5",
|
||||
"starlight-blog": "^0.4.0",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
}
|
1
docs/postcss.config.cjs
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
BIN
docs/public/CRAIG_ROCK.png
Normal file
After Width: | Height: | Size: 1.8 MiB |
1
docs/public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill-rule="evenodd" d="M81 36 64 0 47 36l-1 2-9-10a6 6 0 0 0-9 9l10 10h-2L0 64l36 17h2L28 91a6 6 0 1 0 9 9l9-10 1 2 17 36 17-36v-2l9 10a6 6 0 1 0 9-9l-9-9 2-1 36-17-36-17-2-1 9-9a6 6 0 1 0-9-9l-9 10v-2Zm-17 2-2 5c-4 8-11 15-19 19l-5 2 5 2c8 4 15 11 19 19l2 5 2-5c4-8 11-15 19-19l5-2-5-2c-8-4-15-11-19-19l-2-5Z" clip-rule="evenodd"/><path d="M118 19a6 6 0 0 0-9-9l-3 3a6 6 0 1 0 9 9l3-3Zm-96 4c-2 2-6 2-9 0l-3-3a6 6 0 1 1 9-9l3 3c3 2 3 6 0 9Zm0 82c-2-2-6-2-9 0l-3 3a6 6 0 1 0 9 9l3-3c3-2 3-6 0-9Zm96 4a6 6 0 0 1-9 9l-3-3a6 6 0 1 1 9-9l3 3Z"/><style>path{fill:#000}@media (prefers-color-scheme:dark){path{fill:#fff}}</style></svg>
|
After Width: | Height: | Size: 696 B |
BIN
docs/public/stripe/api-keys.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/public/stripe/db-studio.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
docs/public/stripe/price-ids.png
Normal file
After Width: | Height: | Size: 231 KiB |
BIN
docs/public/stripe/test-product.png
Normal file
After Width: | Height: | Size: 251 KiB |
BIN
docs/src/assets/houston.webp
Normal file
After Width: | Height: | Size: 96 KiB |
8
docs/src/content/config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { i18nSchema } from '@astrojs/starlight/schema';
|
||||
import { docsAndBlogSchema } from 'starlight-blog/schema';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ schema: docsAndBlogSchema }),
|
||||
i18n: defineCollection({ type: 'data', schema: i18nSchema() }),
|
||||
};
|
151
docs/src/content/docs/blog/2023-11-21-coverlettergpt.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: How I Built & Grew CoverLetterGPT to 5,000 Users and $200 MRR
|
||||
date: 2023-11-21
|
||||
tags: ["indiehacker", "saas", "sideproject"]
|
||||
---
|
||||
## Hey, I’m Vince…
|
||||
|
||||

|
||||
|
||||
I’m a self-taught developer that changed careers during the Covid Pandemic. I did it because I wanted a better career, enjoyed programming, and at the same time, had a keen interest in IndieHacking.
|
||||
<!--truncate-->
|
||||
If you’re not aware, IndieHacking is the movement of developers who build potentially profitable side-projects in their spare time. And there are some very successful examples of IndieHackers and “solopreneurs” out there inspiring others, such as [levels.io](http://levels.io) and [Marc Lou](https://twitter.com/marc_louvion).
|
||||
|
||||
This thought of being able to build my own side-project that could generate profit while I slept was always attractive to me.
|
||||
|
||||

|
||||
|
||||
So I’m happy to report that I’ve finally done it with my first software-as-a-service (SaaS) app, [CoverLetterGPT.xyz](http://CoverLetterGPT.xyz), which I launched in March 2023!
|
||||
|
||||
I’ll be the first to admit that the results aren’t spectacular, but they’re still something I’m very proud of:
|
||||
|
||||
- over 5,000 registered users
|
||||
- $203 monthly recurring revenue (MRR)
|
||||
|
||||
Below, I’m going to share with you how I built it (yes, it’s [open-source](https://github.com/vincanger/coverlettergpt)!), how I marketed and monetized it, along with a bunch of helpful resources to help you build your own profitable side-project.
|
||||
|
||||
## What the heck is CoverLetterGPT?
|
||||
|
||||
[CoverLetterGPT.xyz](http://CoverLetterGPT.xyz) was an idea I got after the OpenAI API was released. It’s an app that allows you to upload a PDF of your CV/resumé, along with the job description you’re applying to, and it will generate and edit unique cover letters for you based on this information.
|
||||
|
||||
{% embed https://youtu.be/ZhcFRD9cVrI %}
|
||||
|
||||
It also lets you save and manage your cover letters per each job, making it easy to make and apply to multiple jobs without having to keep copy and pasting all your important info into ChatGPT!
|
||||
|
||||
## What’s the Tech Stack?
|
||||
|
||||

|
||||
|
||||
CoverLetterGPT is entirely open-source, so you can [check out the code](https://github.com/vincanger/coverlettergpt), fork it, learn from it, make your own, submit a PR (I’d love you forever if you did 🙂)… whatever!
|
||||
|
||||
I built it using the [Wasp full-stack framework](https://wasp-lang.dev) which allowed me to ship it about 10x faster.
|
||||
|
||||
Why?
|
||||
|
||||
Because [Wasp](https://wasp-lang.dev) as a framework allows you to describe your app’s core features in a `main.wasp` config file. Then it continually compiles and “glues” these features into a React-ExpressJS-Prisma full-stack app for you.
|
||||
|
||||
All you have to focus on is writing the client and server-side logic, and Wasp will do the boring stuff for you, like authentication & authorization, server config, email sending, and cron jobs.
|
||||
|
||||
BTW, [Wasp](https://wasp-lang.dev) is open-source and free and you can help the project out a ton by starring the repo on GitHub: [https://www.github.com/wasp-lang/wasp](https://www.github.com/wasp-lang/wasp) 🙏
|
||||
|
||||

|
||||
|
||||
{% cta [https://www.github.com/wasp-lang/wasp](https://www.github.com/wasp-lang/wasp) %} ⭐️ Thanks For Your Support 🙏 {% endcta %}
|
||||
|
||||
For the UI, I used [Chakra UI](https://chakra-ui.com/), as I always do. I like that it’s a component-based UI library. This helps me build UI’s a lot faster than I would with Tailwind or vanilla CSS.
|
||||
|
||||
For payments, I used [Stripe](https://www.notion.so/How-I-Built-and-Open-Sourced-CoverLetterGPT-5-000-users-200-MRR-0d32f13fa00a440fb8e08c8dbf2b8a27?pvs=21), (I’ll go into the details of monetization below).
|
||||
|
||||
The Server and Postgres Database are hosted on [https://railway.app](https://railway.app/), with the client on [Netlify.com](http://Netlify.com)’s free tier.
|
||||
|
||||

|
||||
|
||||
By the way, If you’re interested in building your own SaaS with almost the same stack as above, I also built a [free SaaS template](https://github.com/wasp-lang/SaaS-Template-GPT) you can use that will save you days of work!
|
||||
|
||||
## How I Marketed It
|
||||
|
||||
My biggest take-away from this whole project was that open-sourcing it was the best way to market it!
|
||||
|
||||
This seems counter-intuitive, right? Why would making the code available for anyone to see and copy be good for a business? You’re basically rolling out a red carpet for competitors, aren’t you?
|
||||
|
||||
Well, not quite.
|
||||
|
||||
First of all, the number of people who will realistically spend the time and energy launching a direct competitor is low. Also, most people interested in your open-source code want to learn some aspect of it and apply it to their own ideas, not just copy yours directly.
|
||||
|
||||
Secondly, and most importantly, the fact that it’s open-source makes people a lot more receptive to you talking about it.
|
||||
|
||||

|
||||
|
||||
When you present something you’ve built and give people the opportunity to learn from it, they’re much more welcoming! As a result, they’re more likely to upvote it, share it, use it, and recommend it to others.
|
||||
|
||||
This is exactly what happened with CoverLetterGPT! As a result of me sharing the open-source code, it get featured on the [IndieHackers.com](https://www.indiehackers.com/post/whats-new-don-t-build-things-no-one-wants-833ee752ba?utm_source=indie-hackers-emails&utm_campaign=ih-newsletter&utm_medium=email) newsletter (>100k subscribers), shared on blogs, and talked about on social media platforms.
|
||||
|
||||

|
||||
|
||||
And even though it’s a small, simple product, I tried launching it on [Product Hunt](http://producthunt.com), where it also performed considerably well.
|
||||
|
||||
So, all together, these initial efforts combined gave my product a good initial marketing presence. To this day, I haven’t really done much else to market it, except some twitter posts (and this post, if you want to consider it marketing 🤑).
|
||||
|
||||
## How I Monetized It
|
||||
|
||||
When I first launched in March 2023, I didn’t really expect anyone to pay for the product, but I wanted to learn how to use Stripe as a payments processor, thinking that the skills might be useful in the future.
|
||||
|
||||
So I started simple, and just put a one-time payment link for tips. No paywall, no subscriptions. It was entirely free to use with any tip amount welcome.
|
||||
|
||||
To my surprise, tips started coming in, with some as high as $10 dollars!
|
||||
|
||||
This encouraged me to force users to login to use the product, and add a paywall after users used up 3 credits.
|
||||
|
||||
My initial payment options were:
|
||||
|
||||
- $4.95 for a 3 months access
|
||||
- $2.95 for 10 cover letter generations
|
||||
|
||||

|
||||
|
||||
That went reasonably well until I implemented the ability for users to use GPT to make finer edits to their generated cover letters. That’s when I changed my pricing and that’s when better profits started to come in:
|
||||
|
||||
- $5.95 / month subscription with GPT-4
|
||||
- $2.95 / month subscription with GPT-3.5-turbo
|
||||
|
||||
Currently, over 90% of my customers choose the more powerful, more [expensive plan with GPT-4](https://openai.com/pricing), even though the 3 trial credits use the GPT-3.5-turbo model.
|
||||
|
||||
(I also integrated Bitcoin Lightning payments — check out the [repo](https://github.com/vincanger/coverlettergpt) if you want to learn how — but haven’t received any yet.)
|
||||
|
||||
Now, with an MRR of ~$203, my monthly profit of course depends on my costs, which are:
|
||||
|
||||
- Domain Name: $10/year
|
||||
- OpenAI bill: ~ $15/month
|
||||
- Hosting bill: ~ $3/month
|
||||
|
||||
Which leaves me at about ~ $183/month in profits 😀
|
||||
|
||||
## Future Plans
|
||||
|
||||
One of the most surprising aspects about [CoverLetterGPT.xyz](http://CoverLetterGPT.xyz)’s success is that, on the surface, the product is very simple. Also, I’ve done very little in the way of SEO marketing, and haven’t continued to market it much at all. The current growth is mostly organic at this point thanks to my initial marketing efforts.
|
||||
|
||||
But I still have some plans to make it better:
|
||||
|
||||
- buy a better top-level domain (TLD), like [CoverLetterGPT.ai](http://CoverLetterGPT.ai)
|
||||
- add more features, like the ability to generate interview questions based on the cover letters
|
||||
- improve the UX and make it look more “professional”
|
||||
|
||||
If you have any other ideas how I could improve it, drop me a comment, message me on [twitter/x](https://twitter.com/hot_town), or submit a [PR to the repo](https://github.com/vincanger/coverlettergpt).
|
||||
|
||||
## Final Words + More Resources
|
||||
|
||||
My intention with this article was to help others who might be considering launching their own SaaS product. So I hope that’s been the case here. If you still have any questions, don’t hesitate to ask.
|
||||
|
||||
Here are also the most important links from this article along with some further resources that will help in building and marketing your own profitable side-project:
|
||||
|
||||
- 👨💻 [CoverLetterGPT GitHub Repo](https://github.com/vincanger/coverlettergpt)
|
||||
- 💸 [Free Full-Stack SaaS Template w/ Google Auth, Stripe, GPT, & instructions in the README!](https://github.com/wasp-lang/SaaS-Template-GPT)
|
||||
- ✍️ [Initial CoverLetterGPT Reddit Post](https://www.reddit.com/r/webdev/comments/11uh4qo/comment/jco5ggp/?utm_source=share&utm_medium=web2x&context=3)
|
||||
- 🪓 [IndieHackers Feature](https://www.indiehackers.com/post/whats-new-don-t-build-things-no-one-wants-833ee752ba?utm_source=indie-hackers-emails&utm_campaign=ih-newsletter&utm_medium=email)
|
||||
- 💸 [Great Video on how to use Stripe CLI & Webhooks](https://www.youtube.com/watch?v=Psq5N5C-FGo&t=1041s)
|
||||
|
||||
Oh, and if you found these resources useful, don't forget to support Wasp by [starring the repo on GitHub](https://github.com/wasp-lang/wasp)!
|
||||
|
||||

|
||||
|
||||
{% cta [https://www.github.com/wasp-lang/wasp](https://www.github.com/wasp-lang/wasp) %} ⭐️ Thanks For Your Support 🙏 {% endcta %}
|
15
docs/src/content/docs/blog/2023-11-23-post.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: My first blog post
|
||||
date: 2023-11-20
|
||||
authors:
|
||||
- name: Craig Man
|
||||
title: Rock n Roller
|
||||
picture: /CRAIG_ROCK.png
|
||||
tags: ["blog", "post", "saas", "rocknroll"]
|
||||
---
|
||||
|
||||
## Hello
|
||||
|
||||
Hello world!
|
||||
|
||||

|
199
docs/src/content/docs/general/user-permissions.md
Normal file
@@ -0,0 +1,199 @@
|
||||
---
|
||||
title: User Permissions
|
||||
---
|
||||
|
||||
This reference will help you understand how the user permissions work in this template.
|
||||
This includes the user roles, subscription tiers and statuses, and how to authorize access to certain pages and components.
|
||||
|
||||
## User Entity
|
||||
|
||||
The `User` entity within your app is defined in the `main.wasp` file:
|
||||
|
||||
```tsx title="main.wasp" ins="User: {}"
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
email String? @unique
|
||||
password String?
|
||||
createdAt DateTime @default(now())
|
||||
lastActiveTimestamp DateTime @default(now())
|
||||
isAdmin Boolean @default(false)
|
||||
isEmailVerified Boolean @default(false)
|
||||
emailVerificationSentAt DateTime?
|
||||
passwordResetSentAt DateTime?
|
||||
stripeId String?
|
||||
checkoutSessionId String?
|
||||
hasPaid Boolean @default(false)
|
||||
subscriptionTier String?
|
||||
subscriptionStatus String?
|
||||
sendEmail Boolean @default(false)
|
||||
datePaid DateTime?
|
||||
credits Int @default(3)
|
||||
relatedObject RelatedObject[]
|
||||
externalAuthAssociations SocialLogin[]
|
||||
contactFormMessages ContactFormMessage[]
|
||||
referrer Referrer? @relation(fields: [referrerId], references: [id])
|
||||
referrerId Int?
|
||||
psl=}
|
||||
```
|
||||
|
||||
We store all pertinent information to the user, including Auth, Subscription, and Stripe information.
|
||||
|
||||
## Stripe and Subscriptions
|
||||
|
||||
We use Stripe to handle all of our subscription payments. The `User` entity has a number of fields that are related to Stripe and their ability to access features behind the paywall:
|
||||
|
||||
```tsx title="main.wasp" {4-10}
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
//...
|
||||
stripeId String?
|
||||
checkoutSessionId String?
|
||||
hasPaid Boolean @default(false)
|
||||
subscriptionTier String?
|
||||
subscriptionStatus String?
|
||||
datePaid DateTime?
|
||||
credits Int @default(3)
|
||||
//...
|
||||
psl=}
|
||||
```
|
||||
|
||||
- `stripeId`: The Stripe customer ID. This is created by Stripe on checkout and used to identify the customer.
|
||||
- `checkoutSessionId`: The Stripe checkout session ID. This is created by Stripe on checkout and used to identify the checkout session.
|
||||
- `hasPaid`: A boolean that indicates whether the user has paid for a subscription or not.
|
||||
- `subscriptionTier`: The subscription tier the user is on. This is set by the app and is used to determine what features the user has access to. By default, we have two tiers: `hobby-tier` and `pro-tier`.
|
||||
- `subscriptionStatus`: The subscription status of the user. This is set by Stripe and is used to determine whether the user has access to the app or not. By default, we have four statuses: `active`, `past_due`, `canceled`, and `deleted`.
|
||||
- `credits` (optional): You can allow a user to trial your product with a limited number of credits before they have to pay.
|
||||
|
||||
### Subscription Statuses
|
||||
|
||||
In general, we determine if a user has paid for an initial subscription by checking if the `hasPaid` field is true. If it is, we know that the user has paid for a subscription and we can grant them access to the app.
|
||||
|
||||
The `subscriptionStatus` field is set by Stripe within your webhook handler and is used to signify more detailed information on the user's current status. By default, the template handles four statuses: `active`, `past_due`, `canceled`, and `deleted`.
|
||||
|
||||
- When `active` the user has paid for a subscription and has full access to the app.
|
||||
|
||||
- When `canceled`, the user has canceled their subscription and has access to the app until the end of their billing period.
|
||||
|
||||
- When `deleted`, the user has reached the end of their subscription period after canceling and no longer has access to the app.
|
||||
|
||||
- When `past_due`, the user's automatic subscription renewal payment was declined (e.g. their credit card expired). You can choose how to handle this status within your app. For example, you can send the user an email to update their payment information:
|
||||
```tsx title="src/server/webhooks/stripe.ts"
|
||||
import { emailSender } from '@wasp/email/index.js';
|
||||
//...
|
||||
|
||||
if (subscription.status === 'past_due') {
|
||||
const updatedCustomer = await context.entities.User.update({
|
||||
where: {
|
||||
id: customer.id,
|
||||
},
|
||||
data: {
|
||||
subscriptionStatus: 'past_due',
|
||||
},
|
||||
});
|
||||
|
||||
if (updatedCustomer.email) {
|
||||
await emailSender.send({
|
||||
to: updatedCustomer.email,
|
||||
subject: 'Your Payment is Past Due',
|
||||
text: 'Please update your payment information to continue using our service.',
|
||||
html: '...',
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See the client-side authorization section below for more info on how to handle these statuses within your app.
|
||||
|
||||
### Subscription Tiers
|
||||
|
||||
The `subscriptionTier` field is used to determine what features the user has access to.
|
||||
|
||||
By default, we have two tiers: `hobby-tier` and `pro-tier`.
|
||||
|
||||
You can add more tiers by adding more products and price IDs to your Stripe product and updating environment variables in your `.env.server` file as well as the relevant code in your app.
|
||||
|
||||
See the [Stripe Integration Guide](/guides/stripe-integration) for more info on how to do this.
|
||||
|
||||
## User Roles
|
||||
|
||||
At the moment, we have two user roles: `admin` and `user`. This is defined within the `isAdmin` field in the `User` entity:
|
||||
|
||||
```tsx title="main.wasp" {7}
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
email String? @unique
|
||||
password String?
|
||||
createdAt DateTime @default(now())
|
||||
lastActiveTimestamp DateTime @default(now())
|
||||
isAdmin Boolean @default(false)
|
||||
//...
|
||||
psl=}
|
||||
```
|
||||
|
||||
As an Admin, a user has access to the Admin dashboard, along with the user table where they can view and search for users, and edit and update information manually if necessary
|
||||
|
||||
// TODO: add screenshot of user table
|
||||
|
||||
### Client-side Authorization
|
||||
|
||||
All authenticated users have access to the user-facing app, which is the app that sits behind the login. You can easily authorize access to general users from within the `main.wasp` file by adding the `authRequired: true` property to the `page` definition:
|
||||
|
||||
```tsx title="main.wasp" {3}
|
||||
route AccountRoute { path: "/account", to: AccountPage }
|
||||
page AccountPage {
|
||||
authRequired: true,
|
||||
component: import Account from "@client/app/AccountPage"
|
||||
}
|
||||
```
|
||||
|
||||
This will automatically redirect users to the login page if they are not logged in.
|
||||
|
||||
If you want more fine-grained control over what users can access, there are two Wasp-specific options:
|
||||
1. When you define the `authRequired: true` property on the `page` definition, Wasp automatically passes the User object to the page component. Here you can check for certain user properties before authorizing access:
|
||||
|
||||
```tsx title="ExamplePage.tsx" {1}
|
||||
export default function Example({ user }: { user: User })
|
||||
|
||||
if (user.subscriptionStatus === 'past_due') {
|
||||
return (<span>Your subscription is past due. Please update your payment information.</span>)
|
||||
}
|
||||
if (user.subscriptionStatus === 'canceled') {
|
||||
return (<span>Your will susbscription end on 01.01.2024</span>)
|
||||
}
|
||||
if (user.subscriptionStatus === 'active') {
|
||||
return (<span>Thanks so much for your support!</span>)
|
||||
}
|
||||
```
|
||||
|
||||
2. Or you can take advantage of the `useAuth` hook and check for certain user properties before authorizing access to certain pages or components:
|
||||
|
||||
```tsx title="ExamplePage.tsx" {1, 4}
|
||||
import useAuth from '@wasp/auth/useAuth';
|
||||
|
||||
export default function ExampleHomePage() {
|
||||
const { data: user } = useAuth();
|
||||
|
||||
return (
|
||||
<h1> Hi {user.email || 'there'} 👋 </h1>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Server-side Authorization
|
||||
|
||||
You can also authorize access to server-side operations by adding a check to for a logged in user on the `context.user` object which is passed to all operations in Wasp:
|
||||
|
||||
```tsx title="src/server/actions.ts"
|
||||
export const updateCurrentUser: UpdateCurrentUser<...> = async (args, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401); // throw an error if user is not logged in
|
||||
}
|
||||
|
||||
if (context.user.subscriptionStatus === 'past_due') {
|
||||
throw new HttpError(403, 'Your subscription is past due. Please update your payment information.');
|
||||
}
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
|
27
docs/src/content/docs/guides/auth.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Auth Setup
|
||||
---
|
||||
|
||||
Setting up your app's authentication is easy with Wasp. In fact, it's aready set up for your in the `main.wasp` file:
|
||||
|
||||
```tsx title="main.wasp" ins="email: {}" ins="google: {}" ins="gitHub: {}"
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
email: {},
|
||||
google: {},
|
||||
gitHub: {},
|
||||
},
|
||||
onAuthFailedRedirectTo: "/",
|
||||
},
|
||||
```
|
||||
|
||||
The great part is, by defining your auth config in the `main.wasp` file, not only does Wasp handle Auth for you, but you also get auto-generated client components for your app on the fly (aka AuthUI)! You can see them in the `src/client/auth` folder.
|
||||
|
||||
To learn more about using and customizing full-stack Auth with Wasp, including AuthUI, check out the [Wasp Auth docs](https://wasp-lang.dev/docs/auth/overview).
|
||||
|
||||
Since this template has Auth set up for you, you just need to fill in your API keys for your social Auth providers and your Email sender. Follow the integration guides here to do so:
|
||||
- [Google Auth](https://wasp-lang.dev/docs/auth/social-auth/google)
|
||||
- [GitHub Auth](https://wasp-lang.dev/docs/auth/social-auth/github)
|
||||
- [Email verified Auth](https://wasp-lang.dev/docs/auth/email)
|
66
docs/src/content/docs/guides/stripe-integration.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: Stripe Integration
|
||||
---
|
||||
|
||||
This guide will show you how to set up your Stripe account for testing and local development.
|
||||
|
||||
Once you deploy your app, you can follow the same steps, just make sure you're using your live Stripe API keys and product IDs and you are no longer in test mode within the Stripe Dashboard.
|
||||
|
||||
To get started, you'll need to create a Stripe account. You can do that [here](https://dashboard.stripe.com/register).
|
||||
|
||||
## Get your test Stripe API Keys
|
||||
|
||||
Once you've created your account, you'll need to get your test API keys. You can do that by navigating to [https://dashboard.stripe.com/test/apikeys](https://dashboard.stripe.com/test/apikeys) or by going to the [Stripe Dashboard](https://dashboard.stripe.com/test/dashboard) and clicking on the `Developers`.
|
||||
|
||||

|
||||
|
||||
- Click on the `Reveal test key token` button and copy the `Secret key`.
|
||||
- Paste it in your `.env.server` file under `STRIPE_KEY=`
|
||||
|
||||
## Create a Test Product
|
||||
|
||||
To create a test product, go to the test products url [https://dashboard.stripe.com/test/products](https://dashboard.stripe.com/test/products), or after navigating to your dashboard, click the `test mode` toggle.
|
||||
|
||||

|
||||
|
||||
- Click on the `Add a product` button and fill in the relevant information for your product.
|
||||
- Make sure you select `Software as a service (SaaS)` as the product type.
|
||||
- If you want to add different price tiers for the same product, click the `Add another price` button at the buttom.
|
||||
|
||||

|
||||
|
||||
- After you save the product, you'll be directed to the product page.
|
||||
- Copy the price IDs and paste them in the `.env.server` file under `HOBBY_SUBSCRIPTION_PRICE_ID=` and `PRO_SUBSCRIPTION_PRICE_ID=`. Note that if you change the names of the price IDs, you'll need to update your server code to match these names as well
|
||||
|
||||
## Create a Test Customer
|
||||
|
||||
To create a test customer, go to the test customers url [https://dashboard.stripe.com/test/customers](https://dashboard.stripe.com/test/customers).
|
||||
|
||||
- Click on the `Add a customer` button and fill in the relevant information for your test customer.
|
||||
:::note
|
||||
When filling in the test customer email address, use an address you have access to and will use when logging into your SaaS app. This is important because the email address is used to identify the customer when creating a subscription and allows you to manage your test user's payments/subscriptions via the test customer portal
|
||||
:::
|
||||
|
||||
## Install the Stripe CLI
|
||||
|
||||
To install the Stripe CLI with homebrew, run the following command in your terminal:
|
||||
|
||||
```sh
|
||||
brew install stripe/stripe-cli/stripe
|
||||
```
|
||||
|
||||
or for other install scripts or OSes, follow the instructions [here](https://stripe.com/docs/stripe-cli#install).
|
||||
|
||||
Now, let's start the webhook server and get our webhook signing secret.
|
||||
|
||||
```sh
|
||||
stripe listen --forward-to localhost:3001/stripe-webhook
|
||||
```
|
||||
|
||||
You should see a message like this:
|
||||
|
||||
```sh
|
||||
> Ready! You are using Stripe API Version [2023-08-16]. Your webhook signing secret is whsec_8a... (^C to quit)
|
||||
```
|
||||
|
||||
copy this secret to your `.env.server` file under `STRIPE_WEBHOOK_SECRET=`.
|
63
docs/src/content/docs/guides/stripe-testing.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Stripe Testing
|
||||
---
|
||||
|
||||
First, make sure you've set up your Stripe account for testing and local development. You can find the guide [here](/docs/guides/stripe-integration).
|
||||
|
||||
## Testing Webhooks via the Stripe CLI
|
||||
|
||||
- In a new terminal window, run the following command:
|
||||
|
||||
```sh
|
||||
stripe login
|
||||
```
|
||||
|
||||
- start the Stripe CLI webhook forwarding on port 3001 where your Node server is running.
|
||||
|
||||
```sh
|
||||
stripe listen --forward-to localhost:3001/stripe-webhook
|
||||
```
|
||||
|
||||
remember to copy and paste the outputted webhook signing secret (`whsec_...`) into your `.env.server` file under `STRIPE_WEBHOOK_SECRET=` if you haven't already.
|
||||
|
||||
- In another terminal window, trigger a test event:
|
||||
|
||||
```sh
|
||||
stripe trigger payment_intent.succeeded
|
||||
```
|
||||
|
||||
The results of the event firing will be visible in the initial terminal window. You should see messages like this:
|
||||
|
||||
```sh
|
||||
...
|
||||
2023-11-21 09:31:09 --> invoice.paid [evt_1OEpMPILOQf67J5TjrUgRpk4]
|
||||
2023-11-21 09:31:09 <-- [200] POST http://localhost:3001/stripe-webhook [evt_1OEpMPILOQf67J5TjrUgRpk4]
|
||||
2023-11-21 09:31:10 --> invoice.payment_succeeded [evt_1OEpMPILOQf67J5T3MFBr1bq]
|
||||
2023-11-21 09:31:10 <-- [200] POST http://localhost:3001/stripe-webhook [evt_1OEpMPILOQf67J5T3MFBr1bq]
|
||||
2023-11-21 09:31:10 --> checkout.session.completed [evt_1OEpMQILOQf67J5ThTZ0999r]
|
||||
2023-11-21 09:31:11 <-- [200] POST http://localhost:3001/stripe-webhook [evt_1OEpMQILOQf67J5ThTZ0999r]
|
||||
```
|
||||
|
||||
For more info on testing webhooks, check out https://stripe.com/docs/webhooks#test-webhook
|
||||
|
||||
## Testing Payments Webhooks via the Client
|
||||
|
||||
Make sure the **Stripe CLI is running** by following the steps above.
|
||||
You can then test the payment flow via the client by doing the following:
|
||||
|
||||
- Click on the "BUY NOW" button on the homepage. You should be redirected to the checkout page.
|
||||
- Fill in the form with the following test credit card number `4242 4242 4242 4242` and any future date for the expiration date and any 3 digits for the CVC.
|
||||
|
||||
- Click on the "Pay" button. You should be redirected to the success page.
|
||||
|
||||
- Check your terminal window for status messages and logs
|
||||
|
||||
- You can also check your Database via the DB Studio to see if the user entity has been updated by running:
|
||||
|
||||
```sh
|
||||
wasp db studio
|
||||
```
|
||||
|
||||

|
||||
|
||||
- Navigate to `localhost:5555` and click on the `users` table. You should see `hasPaid`is true and `subscriptionStatus` is `active` for the user that just made the purchase.
|
36
docs/src/content/docs/index.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Welcome to Starlight
|
||||
description: Get started building your docs site with Starlight.
|
||||
template: splash
|
||||
hero:
|
||||
tagline: Congrats on setting up a new Starlight project!
|
||||
image:
|
||||
file: ../../assets/houston.webp
|
||||
actions:
|
||||
- text: Example Guide
|
||||
link: /guides/example/
|
||||
icon: right-arrow
|
||||
variant: primary
|
||||
- text: Read the Starlight docs
|
||||
link: https://starlight.astro.build
|
||||
icon: external
|
||||
---
|
||||
|
||||
import { Card, CardGrid } from '@astrojs/starlight/components';
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGrid stagger>
|
||||
<Card title="Update content" icon="pencil">
|
||||
Edit `src/content/docs/index.mdx` to see this page change.
|
||||
</Card>
|
||||
<Card title="Add new content" icon="add-document">
|
||||
Add Markdown or MDX files to `src/content/docs` to create new pages.
|
||||
</Card>
|
||||
<Card title="Configure your site" icon="setting">
|
||||
Edit your `sidebar` and other config in `astro.config.mjs`.
|
||||
</Card>
|
||||
<Card title="Read the docs" icon="open-book">
|
||||
Learn more in [the Starlight Docs](https://starlight.astro.build/).
|
||||
</Card>
|
||||
</CardGrid>
|
62
docs/src/content/docs/start/getting-started.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Getting Started
|
||||
---
|
||||
|
||||
This guide will help you get your new SaaS app up and running.
|
||||
|
||||
## Setting up
|
||||
|
||||
### Install Wasp
|
||||
|
||||
Install Wasp by running this command in your terminal:
|
||||
```sh
|
||||
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
|
||||
```
|
||||
Also install the [Wasp extension for VSCode](https://marketplace.visualstudio.com/items?itemName=wasp-lang.wasp) to get the best DX (syntax highlighting, code scaffolding, autocomplete, etc.)
|
||||
|
||||
### Clone the repo
|
||||
|
||||
Clone this repo by running this command in your terminal:
|
||||
```sh
|
||||
git clone // TODO ADD REPO LINK
|
||||
```
|
||||
|
||||
```sh
|
||||
cd // TODO ADD REPO NAME
|
||||
```
|
||||
|
||||
### Start your DB
|
||||
Make sure you have a Database connected and running. With Wasp, it's super easy:
|
||||
|
||||
First, make sure you have Docker installed and running. If not, download and install it [here](https://www.docker.com/products/docker-desktop/)
|
||||
|
||||
From within the root of the projectn a new terminal window, run:
|
||||
```sh
|
||||
wasp start db
|
||||
```
|
||||
This will start and connect your app to a Postgres database for you. No need to do anything else! 🤯
|
||||
|
||||
In a new terminal window, run:
|
||||
```sh
|
||||
wasp db migrate-dev
|
||||
```
|
||||
This will run the migrations for you and create the tables in your DB.
|
||||
|
||||
If you want to see or manage your DB via Prisma's DB Studio GUI, run:
|
||||
```sh
|
||||
wasp db studio
|
||||
```
|
||||
|
||||
### Start your app
|
||||
In a new terminal window, run:
|
||||
```sh
|
||||
wasp start
|
||||
```
|
||||
This will install all dependencies and start the client and server for you :)
|
||||
|
||||
Go to `localhost:3000` in your browser to view it (your NodeJS server will be running on port `3001`)
|
||||
|
||||
### Further info
|
||||
Check the files for comments containing specific instructions.
|
||||
|
||||
For more info on Wasp as a full-stack React, NodeJS, Prisma framework, check out the [Wasp docs](https://wasp-lang.dev/docs/).
|
110
docs/src/content/docs/start/introduction.md
Normal file
@@ -0,0 +1,110 @@
|
||||
---
|
||||
title: Introduction
|
||||
---
|
||||
|
||||
## Welcome to your new SaaS App!
|
||||
|
||||
// TODO: add a screenshot of the app
|
||||
|
||||
You've decided to build a SaaS app with this template. Great choice! 🎉
|
||||
|
||||
This template is:
|
||||
1. fully open-source
|
||||
2. completely free to use and distribute
|
||||
3. comes with a lot a ton of features out of the box!
|
||||
|
||||
Try it out here: // TODO: add link
|
||||
Check Out the Code: // TODO: add repo link
|
||||
|
||||
## What's inside?
|
||||
|
||||
The template itself is built on top of some very powerful tools and frameworks, including:
|
||||
- 🐝 [Wasp](https://wasp-lang.dev) - a full-stack React, NodeJS, Prisma framework with superpowers
|
||||
- 🚀 [Astro](https://starlight.astro.build/) - Astro's lightweight "Starlight" template for documentation and blog
|
||||
- 💸 [Stripe](https://stripe.com) - for products and payments
|
||||
- 🤖 [OpenAI](https://openai.com) - OpenAI API integrated into the app
|
||||
- 📧 [SendGrid](https://sendgrid.com), [MailGun](https://mailgun.com), or SMTP - for email sending
|
||||
- 💅 [TailwindCSS](https://tailwindcss.com) - for styling
|
||||
- 🧑💼 [TailAdmin](https://tailadmin.com/) - admin dashboard & components for TailwindCSS
|
||||
|
||||
Because we're using Wasp as the full-stack framework, we can leverage a lot of its features to build our SaaS in record time, including:
|
||||
- 🔐 [Full-stack Authentication](https://wasp-lang.dev/docs/auth/overview) - Email verified + social Auth in a few lines of code.
|
||||
- ⛑ [End-to-end Type Safety](https://wasp-lang.dev/docs/data-model/operations/overview) - Type your backend functions and get inferred types on the front-end automatically, without the need to install or configure any third-party libraries. Oh, and type-safe Links, too!
|
||||
- 🤖 [Jobs](https://wasp-lang.dev/docs/language/features#jobs) - Run cron jobs in the background or set up queues simply by defining a function in the config file.
|
||||
- 🚀 [One-command Deploy](https://wasp-lang.dev/docs/advanced/deployment/overview) - Easily deploy via the CLI to [Fly.io](https://fly.io), or to other provides like [Railway](https://railway.app) and [Netlify](https://netlify.com).
|
||||
|
||||
You also get access to Wasp's diverse, helpful community if you get stuck or need help.
|
||||
- 🤝 [Wasp Discord](https://discord.gg/aCamt5wCpS)
|
||||
|
||||
## Getting acquainted with the codebase
|
||||
|
||||
At the root of our project, you will see the following folders and files:
|
||||
```sh
|
||||
.
|
||||
├── .gitignore
|
||||
├── main.wasp # Wasp Config file. You define your app structure here.
|
||||
├── src
|
||||
│ ├── client # Your client code (JS/CSS/HTML) goes here.
|
||||
│ ├── server # Your server code (Node JS) goes here.
|
||||
│ ├── shared # Your shared (runtime independent) code goes here.
|
||||
│ └── .waspignore
|
||||
├── docs # Astro Starlight template for your documentation and blog.
|
||||
├── .env.server # Environment variables for your server code.
|
||||
├── .env.client # Environment variables for your client code.
|
||||
└── .wasproot
|
||||
```
|
||||
|
||||
### Wasp Config file
|
||||
|
||||
The `main.wasp` file is where you define your app structure. You should define everything if your app here first, and then Wasp will generate all the boilerplate code for you.
|
||||
|
||||
Here, we've already defined a number of things:
|
||||
- Auth
|
||||
- Routes and Pages
|
||||
- Prisma Database Models
|
||||
- Server Operations (read and write functions)
|
||||
- Background Jobs
|
||||
|
||||
By defining these things in the config file, Wasp continuously handles the boilerplate necessary with putting all these features together. You just need to focus on the business logic of your app.
|
||||
|
||||
If you want to learn more about Wasp, check out the [Wasp docs](https://wasp-lang.dev/docs/).
|
||||
|
||||
### Client
|
||||
|
||||
The `src/client` folder contains all the code that runs in the browser. It's a standard React app, with a few Wasp-specific things sprinkled in.
|
||||
|
||||
```sh
|
||||
.
|
||||
|
||||
└── client
|
||||
├── admin # Admin dashboard pages and components
|
||||
├── app # Your user-facing app that sits behind the login.
|
||||
├── auth # All auth-related pages and components.
|
||||
├── components # Your shared React components.
|
||||
├── hooks # Your shared React hooks.
|
||||
├── landing-page # Landing page related code
|
||||
├── public # Assets that are publicly accessible, e.g. www.yourdomain.com/banner.png
|
||||
├── static # Assets that you need access to in your code, e.g. import logo from 'static/logo.png'
|
||||
├── App.tsx # Main app component to wrap all child components. Useful for global state, navbars, etc.
|
||||
└── Main.css
|
||||
|
||||
```
|
||||
|
||||
### Server
|
||||
|
||||
The `src/server` folder contains all the code that runs on the server. Wasp compiles everything into a NodeJS server for you. All you have to do is define your server-side functions in the `main.wasp` file, write the logic in a function within `src/server` and Wasp will generate the boilerplate code for you.
|
||||
|
||||
```sh
|
||||
└── server
|
||||
├── auth # Some small auth-related functions to customize the auth flow.
|
||||
├── scripts # Scripts to run via Wasp, e.g. database seeding.
|
||||
├── webhooks # The webhook handler for Stripe.
|
||||
├── workers # Functions that run in the background as Wasp Jobs, e.g. daily stats calculation.
|
||||
├── actions.ts # Your server-side write/mutation functions.
|
||||
├── queries.ts # Your server-side read functions.
|
||||
├── stripeUtils.ts
|
||||
├── static # Assets that you need access to in your code, e.g. import logo from 'static/logo.png'
|
||||
├── types.ts # Main app component to wrap all child components. Useful for global state, navbars, etc.
|
||||
└── Main.css
|
||||
```sh
|
||||
|
2
docs/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
3
docs/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
@@ -93,7 +93,7 @@ entity User {=psl
|
||||
stripeId String?
|
||||
checkoutSessionId String?
|
||||
hasPaid Boolean @default(false)
|
||||
subscriptionType String?
|
||||
subscriptionTier String?
|
||||
subscriptionStatus String?
|
||||
sendEmail Boolean @default(false)
|
||||
datePaid DateTime?
|
||||
|
9
migrations/20231123121547_/migration.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `subscriptionType` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "subscriptionType",
|
||||
ADD COLUMN "subscriptionTier" TEXT;
|
@@ -4,10 +4,10 @@ import getRelatedObjects from '@wasp/queries/getRelatedObjects'
|
||||
import logout from '@wasp/auth/logout';
|
||||
import { useState, Dispatch, SetStateAction } from 'react';
|
||||
import { Link } from '@wasp/router'
|
||||
import { CUSTOMER_PORTAL_LINK } from '../const';
|
||||
import { CUSTOMER_PORTAL_LINK } from '../../shared/const';
|
||||
|
||||
|
||||
export default function Example({ user }: { user: User }) {
|
||||
export default function AccountPage({ user }: { user: User }) {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const { data: relatedObjects, isLoading: isLoadingRelatedObjects } = useQuery(getRelatedObjects);
|
||||
|
@@ -2,6 +2,9 @@ import { AiOutlineCheck } from 'react-icons/ai';
|
||||
import stripePayment from '@wasp/actions/stripePayment';
|
||||
import { useState } from 'react';
|
||||
|
||||
// TODO: fix this page
|
||||
|
||||
|
||||
const prices = [
|
||||
{
|
||||
name: 'Credits',
|
||||
|
@@ -11,7 +11,7 @@ import useAuth from '@wasp/auth/useAuth';
|
||||
import DropdownUser from '../components/DropdownUser';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import stripePayment from '@wasp/actions/stripePayment';
|
||||
import { CUSTOMER_PORTAL_LINK } from '../const';
|
||||
import { CUSTOMER_PORTAL_LINK } from '../../shared/const';
|
||||
|
||||
export default function LandingPage() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { TierIds} from "../../shared/const";
|
||||
|
||||
export const navigation = [
|
||||
{ name: 'Features', href: '#features' },
|
||||
{ name: 'Pricing', href: '#pricing' },
|
||||
@@ -43,14 +45,14 @@ export const features = [
|
||||
export const tiers = [
|
||||
{
|
||||
name: 'Hobby',
|
||||
id: 'hobby-tier',
|
||||
id: TierIds.HOBBY,
|
||||
priceMonthly: '$9.99',
|
||||
description: 'All you need to get started',
|
||||
features: ['Limited monthly usage', 'Basic support'],
|
||||
},
|
||||
{
|
||||
name: 'Pro',
|
||||
id: 'pro-tier',
|
||||
id: TierIds.PRO,
|
||||
priceMonthly: '$19.99',
|
||||
description: 'Our most popular plan',
|
||||
features: ['Unlimited monthly usage', 'Priority customer support'],
|
||||
@@ -58,7 +60,7 @@ export const tiers = [
|
||||
},
|
||||
{
|
||||
name: 'Enterprise',
|
||||
id: 'enterprise-tier',
|
||||
id: TierIds.ENTERPRISE,
|
||||
priceMonthly: '$500',
|
||||
description: 'Big business means big money',
|
||||
features: ['Unlimited monthly usage', '24/7 customer support', 'Advanced analytics'],
|
||||
|
@@ -1,18 +1,26 @@
|
||||
import Stripe from 'stripe';
|
||||
import fetch from 'node-fetch';
|
||||
import HttpError from '@wasp/core/HttpError.js';
|
||||
import type { RelatedObject, User } from '@wasp/entities';
|
||||
import type { GenerateGptResponse, StripePayment } from '@wasp/actions/types';
|
||||
import type { StripePaymentResult, OpenAIResponse } from './types';
|
||||
import { UpdateCurrentUser, SaveReferrer, UpdateUserReferrer, UpdateUserById } from '@wasp/actions/types';
|
||||
import Stripe from 'stripe';
|
||||
import { fetchStripeCustomer, createStripeCheckoutSession } from './stripeUtils.js';
|
||||
import { TierIds } from '../shared/const.js';
|
||||
|
||||
export const stripePayment: StripePayment<string, StripePaymentResult> = async (tier, context) => {
|
||||
if (!context.user || !context.user.email) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
|
||||
const priceId = tier === 'hobby-tier' ? 'HOBBY_SUBSCRIPTION_PRICE_ID' : 'PRO_SUBSCRIPTION_PRICE_ID';
|
||||
let priceId;
|
||||
if (tier === TierIds.HOBBY) {
|
||||
priceId = process.env.HOBBY_SUBSCRIPTION_PRICE_ID!;
|
||||
} else if (tier === TierIds.PRO) {
|
||||
priceId = process.env.PRO_SUBSCRIPTION_PRICE_ID!;
|
||||
} else {
|
||||
throw new HttpError(400, 'Invalid tier');
|
||||
}
|
||||
|
||||
let customer: Stripe.Customer;
|
||||
let session: Stripe.Checkout.Session;
|
||||
|
@@ -28,7 +28,7 @@ export async function createStripeCheckoutSession({ priceId, customerId }: { pri
|
||||
return await stripe.checkout.sessions.create({
|
||||
line_items: [
|
||||
{
|
||||
price: process.env[priceId]!,
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
|
@@ -2,6 +2,7 @@ import express from 'express';
|
||||
import { StripeWebhook } from '@wasp/apis/types';
|
||||
import type { MiddlewareConfigFn } from '@wasp/middleware';
|
||||
import { emailSender } from '@wasp/email/index.js';
|
||||
import { TierIds } from '../../shared/const.js';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
@@ -48,7 +49,7 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
|
||||
data: {
|
||||
hasPaid: true,
|
||||
datePaid: new Date(),
|
||||
subscriptionType: 'hobby',
|
||||
subscriptionTier: TierIds.HOBBY,
|
||||
},
|
||||
});
|
||||
} else if (line_items?.data[0]?.price?.id === process.env.PRO_SUBSCRIPTION_PRICE_ID) {
|
||||
@@ -60,7 +61,7 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
|
||||
data: {
|
||||
hasPaid: true,
|
||||
datePaid: new Date(),
|
||||
subscriptionType: 'pro',
|
||||
subscriptionTier: TierIds.PRO,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -4,3 +4,9 @@ const isDev = process.env.NODE_ENV === 'development';
|
||||
export const CUSTOMER_PORTAL_LINK = isDev
|
||||
? 'https://billing.stripe.com/p/login/test_8wM8x17JN7DT4zC000'
|
||||
: '<insert-prod-link-here>';
|
||||
|
||||
export enum TierIds {
|
||||
HOBBY = 'hobby-tier',
|
||||
PRO = 'pro-tier',
|
||||
ENTERPRISE = 'enterprise-tier',
|
||||
}
|