update docs and stripe credit product ()

This commit is contained in:
vincanger 2024-03-28 17:36:19 +01:00 committed by GitHub
parent 0378b54b68
commit c1b8e953b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 126 additions and 60 deletions

@ -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 :)

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