mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-04-05 02:20:20 +02:00
update docs and stripe credit product (#91)
This commit is contained in:
parent
0378b54b68
commit
c1b8e953b0
@ -1,14 +1,41 @@
|
||||
Thanks so much for considering contributing to Open SaaS 🙏
|
||||
|
||||
## Considerations before Contributing
|
||||
|
||||
### General Considerations
|
||||
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.
|
||||
2. If the issue does already exist, and noone is assigned to it, assign yourself and feel free to begin working on it.
|
||||
|
||||
### How Users Get the Starter Template
|
||||
|
||||
We currently have two ways to pull the template:
|
||||
1. the `use this template` button on the [repo homepage](https://github.com/wasp-lang/open-saas)
|
||||
2. the [Wasp CLI's](https://wasp-lang.dev/docs/quick-start) `wasp new` command
|
||||
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
**This means, that whenever a user pulls the template, they are getting the version present in the most recent commit on `main`**
|
||||
|
||||
Also, If we update Wasp to a new major version, we should also update the `TAG_NAME` in the action.
|
||||
|
||||
### The Default Template vs. the Deployed Site / Docs
|
||||
|
||||
There are two main branches for development:
|
||||
- `main`
|
||||
- `deployed-version`
|
||||
|
||||
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)
|
||||
|
||||
If you want to make changes to the default starter template, base feature branches and Pull Requests off of `main`
|
||||
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`
|
||||
|
||||
## How to contribute
|
||||
Contributing is simple:
|
||||
1. Make sure you've installed and run the app.
|
||||
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.
|
||||
3. If the issue doesn't already exist, create a new one and assign yourself to it.
|
||||
4. Create a new branch for your work.
|
||||
5. Make your changes.
|
||||
6. Commit your changes.
|
||||
7. Push your changes.
|
||||
8. Create a pull request.
|
||||
9. Pray to "Da Boi" while you wait for us to review your PR.
|
||||
10. If you don't know who "Da Boi" is, head back to the [Wasp Discord](https://discord.gg/aCamt5wCpS) and ask around.
|
||||
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.
|
||||
4. Create a pull request.
|
||||
5. Make a "Da Boi" meme while you wait for us to review your PR.
|
||||
6. If you don't know who "Da Boi" is, head back to the [Wasp Discord](https://discord.gg/aCamt5wCpS) and find out :)
|
31
README.md
31
README.md
@ -39,7 +39,38 @@ Because we're using Wasp as the full-stack framework, we can leverage a lot of i
|
||||
You also get access to Wasp's diverse, helpful community if you get stuck or need help.
|
||||
- 🤝 [Wasp Discord](https://discord.gg/aCamt5wCpS)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Simple Instructions
|
||||
|
||||
First, to install the latest version of [Wasp](https://wasp.sh/) on macOS, Linux, or Windows with WSL, run the following command:
|
||||
```bash
|
||||
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
|
||||
```
|
||||
|
||||
Then, create a new SaaS app with the following command:
|
||||
|
||||
```bash
|
||||
wasp new -t saas
|
||||
```
|
||||
|
||||
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!
|
||||
|
||||
### Detailed Instructions
|
||||
|
||||
For everything you need to know about getting started and using this template, check out the [Open SaaS Docs](https://docs.opensaas.sh).
|
||||
|
||||
We've documented everything in great detail, including installation instructions, pulling updates to the template, guides for integrating services, SEO, deployment, and more. 🚀
|
||||
|
||||
## Changes & Contributions
|
||||
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.
|
||||
|
||||
We could always use some help tying up loose ends, so consider [contributing](https://github.com/wasp-lang/open-saas/blob/main/CONTRIBUTING.md)!
|
||||
|
||||
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.
|
||||
|
||||
## Getting Help & Providing Feedback
|
||||
|
||||
There are two ways to get help or provide feedback (and we try to always respond quickly!):
|
||||
1. [Open an issue](https://github.com/wasp-lang/open-saas/issues)
|
||||
2. [Wasp Discord](https://discord.gg/aCamt5wCpS) -- please direct questions to the #🙋questions forum channel
|
||||
|
@ -5,9 +5,10 @@
|
||||
|
||||
# for testing, go to https://dashboard.stripe.com/test/apikeys and get a test stripe key that starts with "sk_test_..."
|
||||
STRIPE_KEY=sk_test_...
|
||||
# to create a test subscription, go to https://dashboard.stripe.com/test/products and click on + Add Product
|
||||
# to create a test product, go to https://dashboard.stripe.com/test/products and click on + Add Product
|
||||
HOBBY_SUBSCRIPTION_PRICE_ID=price_...
|
||||
PRO_SUBSCRIPTION_PRICE_ID=price_...
|
||||
CREDITS_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_...
|
||||
|
||||
|
@ -73,7 +73,7 @@ export default function AccountPage({ user }: { user: User }) {
|
||||
function BuyMoreButton() {
|
||||
return (
|
||||
<div className='ml-4 flex-shrink-0 sm:col-span-1 sm:mt-0'>
|
||||
<Link to='/' hash='pricing' className='font-medium text-sm text-indigo-600 hover:text-indigo-500'>
|
||||
<Link to='/pricing' className='font-medium text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-500'>
|
||||
Buy More/Upgrade
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -10,24 +10,24 @@ export const tiers = [
|
||||
{
|
||||
name: 'Hobby',
|
||||
id: TierIds.HOBBY,
|
||||
priceMonthly: '$9.99',
|
||||
price: '$9.99',
|
||||
description: 'All you need to get started',
|
||||
features: ['Limited monthly usage', 'Basic support'],
|
||||
},
|
||||
{
|
||||
name: 'Pro',
|
||||
id: TierIds.PRO,
|
||||
priceMonthly: '$19.99',
|
||||
price: '$19.99',
|
||||
description: 'Our most popular plan',
|
||||
features: ['Unlimited monthly usage', 'Priority customer support'],
|
||||
bestDeal: true,
|
||||
},
|
||||
{
|
||||
name: 'Enterprise',
|
||||
id: TierIds.ENTERPRISE,
|
||||
priceMonthly: '$500',
|
||||
description: 'Big business means big money',
|
||||
features: ['Unlimited monthly usage', '24/7 customer support', 'Advanced analytics'],
|
||||
name: '10 Credits',
|
||||
id: TierIds.CREDITS,
|
||||
price: '$9.99',
|
||||
description: 'One-time purchase of 10 credits for your account',
|
||||
features: ['Use credits for e.g. OpenAI API calls', 'No expiration date'],
|
||||
},
|
||||
];
|
||||
|
||||
@ -100,10 +100,10 @@ const PricingPage = () => {
|
||||
</div>
|
||||
<p className='mt-4 text-sm leading-6 text-gray-600 dark:text-white'>{tier.description}</p>
|
||||
<p className='mt-6 flex items-baseline gap-x-1 dark:text-white'>
|
||||
<span className='text-4xl font-bold tracking-tight text-gray-900 dark:text-white'>
|
||||
{tier.priceMonthly}
|
||||
<span className='text-4xl font-bold tracking-tight text-gray-900 dark:text-white'>{tier.price}</span>
|
||||
<span className='text-sm font-semibold leading-6 text-gray-600 dark:text-white'>
|
||||
{tier.id !== TierIds.CREDITS && '/month'}
|
||||
</span>
|
||||
<span className='text-sm font-semibold leading-6 text-gray-600 dark:text-white'>/month</span>
|
||||
</p>
|
||||
<ul role='list' className='mt-8 space-y-3 text-sm leading-6 text-gray-600 dark:text-white'>
|
||||
{tier.features.map((feature) => (
|
||||
@ -120,27 +120,19 @@ const PricingPage = () => {
|
||||
aria-describedby='manage-subscription'
|
||||
className={cn(
|
||||
'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',
|
||||
{
|
||||
'opacity-50 cursor-not-allowed': tier.id === 'enterprise-tier',
|
||||
'opacity-100 cursor-pointer': tier.id !== 'enterprise-tier',
|
||||
},
|
||||
{
|
||||
'bg-yellow-500 text-white hover:text-white shadow-sm hover:bg-yellow-400': tier.bestDeal,
|
||||
'text-gray-600 ring-1 ring-inset ring-purple-200 hover:ring-purple-400': !tier.bestDeal,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{tier.id === 'enterprise-tier' ? 'Contact us' : 'Manage Subscription'}
|
||||
Manage Subscription
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleBuyNowClick(tier.id)}
|
||||
aria-describedby={tier.id}
|
||||
className={cn(
|
||||
{
|
||||
'opacity-50 cursor-not-allowed': tier.id === 'enterprise-tier',
|
||||
'opacity-100 cursor-pointer': tier.id !== 'enterprise-tier',
|
||||
},
|
||||
{
|
||||
'bg-yellow-500 text-white hover:text-white shadow-sm hover:bg-yellow-400': tier.bestDeal,
|
||||
'text-gray-600 ring-1 ring-inset ring-purple-200 hover:ring-purple-400': !tier.bestDeal,
|
||||
@ -151,7 +143,7 @@ const PricingPage = () => {
|
||||
'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'
|
||||
)}
|
||||
>
|
||||
{tier.id === 'enterprise-tier' ? 'Contact us' : !!user ? 'Buy plan' : 'Log in to buy plan'}
|
||||
{!!user ? 'Buy plan' : 'Log in to buy plan'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
@ -42,8 +42,10 @@ export const stripePayment: StripePayment<string, StripePaymentResult> = async (
|
||||
priceId = process.env.HOBBY_SUBSCRIPTION_PRICE_ID!;
|
||||
} else if (tier === TierIds.PRO) {
|
||||
priceId = process.env.PRO_SUBSCRIPTION_PRICE_ID!;
|
||||
} else if (tier === TierIds.CREDITS) {
|
||||
priceId = process.env.CREDITS_PRICE_ID!;
|
||||
} else {
|
||||
throw new HttpError(400, 'Invalid tier');
|
||||
throw new HttpError(404, 'Invalid tier');
|
||||
}
|
||||
|
||||
let customer: Stripe.Customer;
|
||||
@ -53,9 +55,12 @@ export const stripePayment: StripePayment<string, StripePaymentResult> = async (
|
||||
session = await createStripeCheckoutSession({
|
||||
priceId,
|
||||
customerId: customer.id,
|
||||
mode: tier === TierIds.CREDITS ? 'payment' : 'subscription',
|
||||
});
|
||||
} catch (error: any) {
|
||||
throw new HttpError(500, error.message);
|
||||
const statusCode = error.statusCode || 500;
|
||||
const errorMessage = error.message || 'Internal server error';
|
||||
throw new HttpError(statusCode, errorMessage);
|
||||
}
|
||||
|
||||
await context.entities.User.update({
|
||||
|
@ -24,7 +24,15 @@ export async function fetchStripeCustomer(customerEmail: string) {
|
||||
return customer;
|
||||
}
|
||||
|
||||
export async function createStripeCheckoutSession({ priceId, customerId }: { priceId: string; customerId: string }) {
|
||||
export async function createStripeCheckoutSession({
|
||||
priceId,
|
||||
customerId,
|
||||
mode,
|
||||
}: {
|
||||
priceId: string;
|
||||
customerId: string;
|
||||
mode: 'subscription' | 'payment';
|
||||
}) {
|
||||
return await stripe.checkout.sessions.create({
|
||||
line_items: [
|
||||
{
|
||||
@ -32,7 +40,7 @@ export async function createStripeCheckoutSession({ priceId, customerId }: { pri
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: 'subscription',
|
||||
mode: mode,
|
||||
success_url: `${DOMAIN}/checkout?success=true`,
|
||||
cancel_url: `${DOMAIN}/checkout?canceled=true`,
|
||||
automatic_tax: { enabled: true },
|
||||
|
@ -36,8 +36,13 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
|
||||
expand: ['line_items'],
|
||||
});
|
||||
|
||||
/**
|
||||
* here are your products, both subscriptions and one-time payments.
|
||||
* make sure to configure them in the Stripe dashboard first!
|
||||
* see: https://docs.opensaas.sh/guides/stripe-integration/
|
||||
*/
|
||||
if (line_items?.data[0]?.price?.id === process.env.HOBBY_SUBSCRIPTION_PRICE_ID) {
|
||||
console.log('Hobby subscription purchased ');
|
||||
console.log('Hobby subscription purchased');
|
||||
await context.entities.User.updateMany({
|
||||
where: {
|
||||
stripeId: userStripeId,
|
||||
@ -49,7 +54,7 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
|
||||
},
|
||||
});
|
||||
} else if (line_items?.data[0]?.price?.id === process.env.PRO_SUBSCRIPTION_PRICE_ID) {
|
||||
console.log('Pro subscription purchased ');
|
||||
console.log('Pro subscription purchased');
|
||||
await context.entities.User.updateMany({
|
||||
where: {
|
||||
stripeId: userStripeId,
|
||||
@ -60,27 +65,22 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
|
||||
subscriptionTier: TierIds.PRO,
|
||||
},
|
||||
});
|
||||
} else if (line_items?.data[0]?.price?.id === process.env.CREDITS_PRICE_ID) {
|
||||
console.log('Credits purchased');
|
||||
await context.entities.User.updateMany({
|
||||
where: {
|
||||
stripeId: userStripeId,
|
||||
},
|
||||
data: {
|
||||
credits: {
|
||||
increment: 10,
|
||||
},
|
||||
datePaid: new Date(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
response.status(404).send('Invalid product');
|
||||
}
|
||||
|
||||
/**
|
||||
* and here is an example of handling a product that is not a subscription
|
||||
* in this case, we are adding 10 credits to the user's account
|
||||
* make sure to configure it in the Stripe dashboard first!
|
||||
*/
|
||||
|
||||
// if (line_items?.data[0]?.price?.id === process.env.CREDITS_PRICE_ID) {
|
||||
// console.log('Credits purchased: ');
|
||||
// await context.entities.User.updateMany({
|
||||
// where: {
|
||||
// stripeId: userStripeId,
|
||||
// },
|
||||
// data: {
|
||||
// credits: {
|
||||
// increment: 10,
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
} else if (event.type === 'invoice.paid') {
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
const periodStart = new Date(invoice.period_start * 1000);
|
||||
@ -107,9 +107,11 @@ export const stripeWebhook: StripeWebhook = async (request, response, context) =
|
||||
},
|
||||
});
|
||||
}
|
||||
// you'll want to make a check on the front end to see if the subscription is past due
|
||||
// and then prompt the user to update their payment method
|
||||
// this is useful if the user's card expires or is canceled and automatic subscription renewal fails
|
||||
/**
|
||||
* you'll want to make a check on the front end to see if the subscription is past due
|
||||
* and then prompt the user to update their payment method
|
||||
* this is useful if the user's card expires or is canceled and automatic subscription renewal fails
|
||||
*/
|
||||
if (subscription.status === 'past_due') {
|
||||
console.log('Subscription past due: ', userStripeId);
|
||||
await context.entities.User.updateMany({
|
||||
|
@ -3,7 +3,7 @@ import { z } from 'zod';
|
||||
export enum TierIds {
|
||||
HOBBY = 'hobby-tier',
|
||||
PRO = 'pro-tier',
|
||||
ENTERPRISE = 'enterprise-tier',
|
||||
CREDITS = 'credits',
|
||||
}
|
||||
|
||||
export const DOCS_URL = 'https://docs.opensaas.sh';
|
||||
|
Loading…
x
Reference in New Issue
Block a user