Skip to content

Commit c1b8e95

Browse files
authored
update docs and stripe credit product (wasp-lang#91)
1 parent 0378b54 commit c1b8e95

File tree

9 files changed

+126
-60
lines changed

9 files changed

+126
-60
lines changed

CONTRIBUTING.md

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,41 @@
11
Thanks so much for considering contributing to Open SaaS 🙏
22

3+
## Considerations before Contributing
4+
5+
### General Considerations
6+
1. If there's something you'd like to add, and the issue doesn't already exist, create a new one and assign yourself to it. Wait until we've agreed on a plan of action before beginning your work.
7+
2. If the issue does already exist, and noone is assigned to it, assign yourself and feel free to begin working on it.
8+
9+
### How Users Get the Starter Template
10+
11+
We currently have two ways to pull the template:
12+
1. the `use this template` button on the [repo homepage](https://github.com/wasp-lang/open-saas)
13+
2. the [Wasp CLI's](https://wasp-lang.dev/docs/quick-start) `wasp new` command
14+
15+
When pulling the template via `wasp new`, the Wasp CLI looks for a tag `wasp-{CURRENT_VERSION}-template` associated with a specific commit on the Open SaaS repo.
16+
17+
In order to keep this tag up to date, we've created a github action, `.github/workflows/retag-commit.yml`, that automatically reassigns the tag (defined as `TAG_NAME` in the action) to the most recent commit on `main`.
18+
19+
**This means, that whenever a user pulls the template, they are getting the version present in the most recent commit on `main`**
20+
21+
Also, If we update Wasp to a new major version, we should also update the `TAG_NAME` in the action.
22+
23+
### The Default Template vs. the Deployed Site / Docs
24+
25+
There are two main branches for development:
26+
- `main`
27+
- `deployed-version`
28+
29+
The default, clean template that users get when cloning the starter lives on `main`, while `deployed-version` is what you see when you go to [OpenSaaS.sh](https://opensaas.sh) and the [docs](https://docs.opensaas.sh)
30+
31+
If you want to make changes to the default starter template, base feature branches and Pull Requests off of `main`
32+
If you want to make changes to the OpenSaaS.sh site or it's Documentation, base feature branches and Pull Requests off of `deployed-version`
33+
334
## How to contribute
435
Contributing is simple:
536
1. Make sure you've installed and run the app.
637
2. Find something you'd like to work on. Check out the [issues](https://github.com/wasp-lang/open-saas/issues) or contact us on the [Wasp Discord](https://discord.gg/aCamt5wCpS) to discuss.
7-
3. If the issue doesn't already exist, create a new one and assign yourself to it.
8-
4. Create a new branch for your work.
9-
5. Make your changes.
10-
6. Commit your changes.
11-
7. Push your changes.
12-
8. Create a pull request.
13-
9. Pray to "Da Boi" while you wait for us to review your PR.
14-
10. If you don't know who "Da Boi" is, head back to the [Wasp Discord](https://discord.gg/aCamt5wCpS) and ask around.
38+
3. Create a new feature branch for your work. See [above](#the-default-template-vs-the-deployed-site--docs) for which branch to base your feature branch off of.
39+
4. Create a pull request.
40+
5. Make a "Da Boi" meme while you wait for us to review your PR.
41+
6. If you don't know who "Da Boi" is, head back to the [Wasp Discord](https://discord.gg/aCamt5wCpS) and find out :)

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,38 @@ Because we're using Wasp as the full-stack framework, we can leverage a lot of i
3939
You also get access to Wasp's diverse, helpful community if you get stuck or need help.
4040
- 🤝 [Wasp Discord](https://discord.gg/aCamt5wCpS)
4141

42+
## Getting Started
4243

44+
### Simple Instructions
45+
46+
First, to install the latest version of [Wasp](https://wasp.sh/) on macOS, Linux, or Windows with WSL, run the following command:
47+
```bash
48+
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
49+
```
50+
51+
Then, create a new SaaS app with the following command:
52+
53+
```bash
54+
wasp new -t saas
55+
```
56+
57+
This will clone a **clean copy of the Open SaaS template** into a new directory, and you can start building your SaaS app right away!
58+
59+
### Detailed Instructions
60+
61+
For everything you need to know about getting started and using this template, check out the [Open SaaS Docs](https://docs.opensaas.sh).
62+
63+
We've documented everything in great detail, including installation instructions, pulling updates to the template, guides for integrating services, SEO, deployment, and more. 🚀
64+
65+
## Changes & Contributions
4366
Note that we've tried to get as many of the core features of a SaaS app into this template as possible, but there still might be some missing features or functionality.
4467

4568
We could always use some help tying up loose ends, so consider [contributing](https://github.com/wasp-lang/open-saas/blob/main/CONTRIBUTING.md)!
69+
70+
As there are a few things to know and consider when contributing, please make sure to read the [CONTRIBUTING.md](https://github.com/wasp-lang/open-saas/blob/main/CONTRIBUTING.md) in this Repo.
71+
72+
## Getting Help & Providing Feedback
73+
74+
There are two ways to get help or provide feedback (and we try to always respond quickly!):
75+
1. [Open an issue](https://github.com/wasp-lang/open-saas/issues)
76+
2. [Wasp Discord](https://discord.gg/aCamt5wCpS) -- please direct questions to the #🙋questions forum channel

app/.env.server.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66
# for testing, go to https://dashboard.stripe.com/test/apikeys and get a test stripe key that starts with "sk_test_..."
77
STRIPE_KEY=sk_test_...
8-
# to create a test subscription, go to https://dashboard.stripe.com/test/products and click on + Add Product
8+
# to create a test product, go to https://dashboard.stripe.com/test/products and click on + Add Product
99
HOBBY_SUBSCRIPTION_PRICE_ID=price_...
1010
PRO_SUBSCRIPTION_PRICE_ID=price_...
11+
CREDITS_PRICE_ID=price_...
1112
# 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
1213
STRIPE_WEBHOOK_SECRET=whsec_...
1314

app/src/client/app/AccountPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export default function AccountPage({ user }: { user: User }) {
7373
function BuyMoreButton() {
7474
return (
7575
<div className='ml-4 flex-shrink-0 sm:col-span-1 sm:mt-0'>
76-
<Link to='/' hash='pricing' className='font-medium text-sm text-indigo-600 hover:text-indigo-500'>
76+
<Link to='/pricing' className='font-medium text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-500'>
7777
Buy More/Upgrade
7878
</Link>
7979
</div>

app/src/client/app/PricingPage.tsx

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,24 @@ export const tiers = [
1010
{
1111
name: 'Hobby',
1212
id: TierIds.HOBBY,
13-
priceMonthly: '$9.99',
13+
price: '$9.99',
1414
description: 'All you need to get started',
1515
features: ['Limited monthly usage', 'Basic support'],
1616
},
1717
{
1818
name: 'Pro',
1919
id: TierIds.PRO,
20-
priceMonthly: '$19.99',
20+
price: '$19.99',
2121
description: 'Our most popular plan',
2222
features: ['Unlimited monthly usage', 'Priority customer support'],
2323
bestDeal: true,
2424
},
2525
{
26-
name: 'Enterprise',
27-
id: TierIds.ENTERPRISE,
28-
priceMonthly: '$500',
29-
description: 'Big business means big money',
30-
features: ['Unlimited monthly usage', '24/7 customer support', 'Advanced analytics'],
26+
name: '10 Credits',
27+
id: TierIds.CREDITS,
28+
price: '$9.99',
29+
description: 'One-time purchase of 10 credits for your account',
30+
features: ['Use credits for e.g. OpenAI API calls', 'No expiration date'],
3131
},
3232
];
3333

@@ -100,10 +100,10 @@ const PricingPage = () => {
100100
</div>
101101
<p className='mt-4 text-sm leading-6 text-gray-600 dark:text-white'>{tier.description}</p>
102102
<p className='mt-6 flex items-baseline gap-x-1 dark:text-white'>
103-
<span className='text-4xl font-bold tracking-tight text-gray-900 dark:text-white'>
104-
{tier.priceMonthly}
103+
<span className='text-4xl font-bold tracking-tight text-gray-900 dark:text-white'>{tier.price}</span>
104+
<span className='text-sm font-semibold leading-6 text-gray-600 dark:text-white'>
105+
{tier.id !== TierIds.CREDITS && '/month'}
105106
</span>
106-
<span className='text-sm font-semibold leading-6 text-gray-600 dark:text-white'>/month</span>
107107
</p>
108108
<ul role='list' className='mt-8 space-y-3 text-sm leading-6 text-gray-600 dark:text-white'>
109109
{tier.features.map((feature) => (
@@ -120,27 +120,19 @@ const PricingPage = () => {
120120
aria-describedby='manage-subscription'
121121
className={cn(
122122
'mt-8 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 focus-visible:outline focus-visible:outline-2 focus-visible:outline-yellow-400',
123-
{
124-
'opacity-50 cursor-not-allowed': tier.id === 'enterprise-tier',
125-
'opacity-100 cursor-pointer': tier.id !== 'enterprise-tier',
126-
},
127123
{
128124
'bg-yellow-500 text-white hover:text-white shadow-sm hover:bg-yellow-400': tier.bestDeal,
129125
'text-gray-600 ring-1 ring-inset ring-purple-200 hover:ring-purple-400': !tier.bestDeal,
130126
}
131127
)}
132128
>
133-
{tier.id === 'enterprise-tier' ? 'Contact us' : 'Manage Subscription'}
129+
Manage Subscription
134130
</a>
135131
) : (
136132
<button
137133
onClick={() => handleBuyNowClick(tier.id)}
138134
aria-describedby={tier.id}
139135
className={cn(
140-
{
141-
'opacity-50 cursor-not-allowed': tier.id === 'enterprise-tier',
142-
'opacity-100 cursor-pointer': tier.id !== 'enterprise-tier',
143-
},
144136
{
145137
'bg-yellow-500 text-white hover:text-white shadow-sm hover:bg-yellow-400': tier.bestDeal,
146138
'text-gray-600 ring-1 ring-inset ring-purple-200 hover:ring-purple-400': !tier.bestDeal,
@@ -151,7 +143,7 @@ const PricingPage = () => {
151143
'mt-8 block rounded-md py-2 px-3 text-center text-sm dark:text-white font-semibold leading-6 focus-visible:outline focus-visible:outline-2 focus-visible:outline-yellow-400'
152144
)}
153145
>
154-
{tier.id === 'enterprise-tier' ? 'Contact us' : !!user ? 'Buy plan' : 'Log in to buy plan'}
146+
{!!user ? 'Buy plan' : 'Log in to buy plan'}
155147
</button>
156148
)}
157149
</div>

app/src/server/actions.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ export const stripePayment: StripePayment<string, StripePaymentResult> = async (
4242
priceId = process.env.HOBBY_SUBSCRIPTION_PRICE_ID!;
4343
} else if (tier === TierIds.PRO) {
4444
priceId = process.env.PRO_SUBSCRIPTION_PRICE_ID!;
45+
} else if (tier === TierIds.CREDITS) {
46+
priceId = process.env.CREDITS_PRICE_ID!;
4547
} else {
46-
throw new HttpError(400, 'Invalid tier');
48+
throw new HttpError(404, 'Invalid tier');
4749
}
4850

4951
let customer: Stripe.Customer;
@@ -53,9 +55,12 @@ export const stripePayment: StripePayment<string, StripePaymentResult> = async (
5355
session = await createStripeCheckoutSession({
5456
priceId,
5557
customerId: customer.id,
58+
mode: tier === TierIds.CREDITS ? 'payment' : 'subscription',
5659
});
5760
} catch (error: any) {
58-
throw new HttpError(500, error.message);
61+
const statusCode = error.statusCode || 500;
62+
const errorMessage = error.message || 'Internal server error';
63+
throw new HttpError(statusCode, errorMessage);
5964
}
6065

6166
await context.entities.User.update({

app/src/server/payments/stripeUtils.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,23 @@ export async function fetchStripeCustomer(customerEmail: string) {
2424
return customer;
2525
}
2626

27-
export async function createStripeCheckoutSession({ priceId, customerId }: { priceId: string; customerId: string }) {
27+
export async function createStripeCheckoutSession({
28+
priceId,
29+
customerId,
30+
mode,
31+
}: {
32+
priceId: string;
33+
customerId: string;
34+
mode: 'subscription' | 'payment';
35+
}) {
2836
return await stripe.checkout.sessions.create({
2937
line_items: [
3038
{
3139
price: priceId,
3240
quantity: 1,
3341
},
3442
],
35-
mode: 'subscription',
43+
mode: mode,
3644
success_url: `${DOMAIN}/checkout?success=true`,
3745
cancel_url: `${DOMAIN}/checkout?canceled=true`,
3846
automatic_tax: { enabled: true },

app/src/server/webhooks/stripe.ts

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
3636
expand: ['line_items'],
3737
});
3838

39+
/**
40+
* here are your products, both subscriptions and one-time payments.
41+
* make sure to configure them in the Stripe dashboard first!
42+
* see: https://docs.opensaas.sh/guides/stripe-integration/
43+
*/
3944
if (line_items?.data[0]?.price?.id === process.env.HOBBY_SUBSCRIPTION_PRICE_ID) {
40-
console.log('Hobby subscription purchased ');
45+
console.log('Hobby subscription purchased');
4146
await context.entities.User.updateMany({
4247
where: {
4348
stripeId: userStripeId,
@@ -49,7 +54,7 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
4954
},
5055
});
5156
} else if (line_items?.data[0]?.price?.id === process.env.PRO_SUBSCRIPTION_PRICE_ID) {
52-
console.log('Pro subscription purchased ');
57+
console.log('Pro subscription purchased');
5358
await context.entities.User.updateMany({
5459
where: {
5560
stripeId: userStripeId,
@@ -60,27 +65,22 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
6065
subscriptionTier: TierIds.PRO,
6166
},
6267
});
68+
} else if (line_items?.data[0]?.price?.id === process.env.CREDITS_PRICE_ID) {
69+
console.log('Credits purchased');
70+
await context.entities.User.updateMany({
71+
where: {
72+
stripeId: userStripeId,
73+
},
74+
data: {
75+
credits: {
76+
increment: 10,
77+
},
78+
datePaid: new Date(),
79+
},
80+
});
81+
} else {
82+
response.status(404).send('Invalid product');
6383
}
64-
65-
/**
66-
* and here is an example of handling a product that is not a subscription
67-
* in this case, we are adding 10 credits to the user's account
68-
* make sure to configure it in the Stripe dashboard first!
69-
*/
70-
71-
// if (line_items?.data[0]?.price?.id === process.env.CREDITS_PRICE_ID) {
72-
// console.log('Credits purchased: ');
73-
// await context.entities.User.updateMany({
74-
// where: {
75-
// stripeId: userStripeId,
76-
// },
77-
// data: {
78-
// credits: {
79-
// increment: 10,
80-
// },
81-
// },
82-
// });
83-
// }
8484
} else if (event.type === 'invoice.paid') {
8585
const invoice = event.data.object as Stripe.Invoice;
8686
const periodStart = new Date(invoice.period_start * 1000);
@@ -107,9 +107,11 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
107107
},
108108
});
109109
}
110-
// you'll want to make a check on the front end to see if the subscription is past due
111-
// and then prompt the user to update their payment method
112-
// this is useful if the user's card expires or is canceled and automatic subscription renewal fails
110+
/**
111+
* you'll want to make a check on the front end to see if the subscription is past due
112+
* and then prompt the user to update their payment method
113+
* this is useful if the user's card expires or is canceled and automatic subscription renewal fails
114+
*/
113115
if (subscription.status === 'past_due') {
114116
console.log('Subscription past due: ', userStripeId);
115117
await context.entities.User.updateMany({

app/src/shared/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { z } from 'zod';
33
export enum TierIds {
44
HOBBY = 'hobby-tier',
55
PRO = 'pro-tier',
6-
ENTERPRISE = 'enterprise-tier',
6+
CREDITS = 'credits',
77
}
88

99
export const DOCS_URL = 'https://docs.opensaas.sh';

0 commit comments

Comments
 (0)