mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-03-29 11:12:19 +01:00
small pricing updates
This commit is contained in:
parent
cb3d75c0b6
commit
48f8c5ce73
@ -6,7 +6,7 @@
|
||||
+ "scripts": {
|
||||
+ "env:pull": "npx dotenv-vault@latest pull development .env.server",
|
||||
+ "env:push": "npx dotenv-vault@latest push development .env.server",
|
||||
+ "deploy": "REACT_APP_STRIPE_CUSTOMER_PORTAL=https://billing.stripe.com/p/login/test_8wM8x17JN7DT4zC000 REACT_APP_GOOGLE_ANALYTICS_ID=G-H3LSJCK95H wasp deploy fly deploy"
|
||||
+ "deploy": "REACT_APP_GOOGLE_ANALYTICS_ID=G-H3LSJCK95H wasp deploy fly deploy"
|
||||
+ },
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.523.0",
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
interface PaymentPlanCard {
|
||||
name: string;
|
||||
@@ -83,7 +84,7 @@
|
||||
@@ -82,7 +83,7 @@
|
||||
}
|
||||
|
||||
if (!customerPortalUrl) {
|
||||
@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
window.open(customerPortalUrl, '_blank');
|
||||
@@ -97,11 +98,18 @@
|
||||
@@ -96,11 +97,18 @@
|
||||
Pick your <span className='text-yellow-500'>pricing</span>
|
||||
</h2>
|
||||
</div>
|
||||
@ -41,7 +41,7 @@
|
||||
<div className='isolate mx-auto mt-16 grid max-w-md grid-cols-1 gap-y-8 lg:gap-x-8 sm:mt-20 lg:mx-0 lg:max-w-none lg:grid-cols-3'>
|
||||
{Object.values(PaymentPlanId).map((planId) => (
|
||||
<div
|
||||
@@ -188,7 +196,7 @@
|
||||
@@ -187,7 +195,7 @@
|
||||
)}
|
||||
disabled={isPaymentLoading}
|
||||
>
|
||||
|
@ -14,7 +14,7 @@ Stripe is the industry standard, is more configurable, and has cheaper fees.
|
||||
Lemon Squeezy acts a [Merchant of Record](https://www.lemonsqueezy.com/reporting/merchant-of-record). This means they take care of paying taxes in multiple countries for you, but charge higher fees per transaction.
|
||||
:::
|
||||
|
||||
# Important First Steps
|
||||
## Important First Steps
|
||||
|
||||
First, go to `/src/payment/paymentProcessor.ts` and choose which payment processor you'd like to use, e.g. Stripe or Lemon Squeezy:
|
||||
|
||||
@ -41,7 +41,7 @@ At this point, you can delete:
|
||||
|
||||
Now your code is ready to go with your preferred payment processor and it's time to configure your payment processor's API keys, products, and other settings.
|
||||
|
||||
# Stripe
|
||||
## Stripe
|
||||
|
||||
First, you'll need to create a Stripe account. You can do that [here](https://dashboard.stripe.com/register).
|
||||
|
||||
@ -51,7 +51,7 @@ We've packed in a ton of features and love into this SaaS starter, and offer it
|
||||
If you're finding this template and its guides useful, consider giving us [a star on GitHub](https://github.com/wasp-lang/wasp)
|
||||
:::
|
||||
|
||||
## Get your test Stripe API Keys
|
||||
### 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`.
|
||||
|
||||
@ -60,7 +60,7 @@ Once you've created your account, you'll need to get your test API keys. You can
|
||||
- Click on the `Reveal test key token` button and copy the `Secret key`.
|
||||
- Paste it in your `.env.server` file under `STRIPE_API_KEY=`
|
||||
|
||||
## Create Test Products
|
||||
### Create Test Products
|
||||
|
||||
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.
|
||||
|
||||
@ -80,7 +80,7 @@ To create a test product, go to the test products url [https://dashboard.stripe.
|
||||
- As well as a one-time payment product/credits-based environment variable, `PAYMENTS_CREDITS_10_PLAN_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
|
||||
### 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).
|
||||
|
||||
@ -89,7 +89,7 @@ To create a test customer, go to the test customers url [https://dashboard.strip
|
||||
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
|
||||
:::
|
||||
|
||||
## Get your Customer Portal Link
|
||||
### Get your Customer Portal Link
|
||||
|
||||
Go to https://dashboard.stripe.com/test/settings/billing/portal in the Stripe Dashboard and activate and copy the `Customer portal link`. Paste it in your `.env.server` file:
|
||||
|
||||
@ -97,7 +97,7 @@ Go to https://dashboard.stripe.com/test/settings/billing/portal in the Stripe Da
|
||||
STRIPE_CUSTOMER_PORTAL_URL=<your-test-customer-portal-link>
|
||||
```
|
||||
|
||||
## Install the Stripe CLI
|
||||
### Install the Stripe CLI
|
||||
|
||||
To install the Stripe CLI with homebrew, run the following command in your terminal:
|
||||
|
||||
@ -131,7 +131,7 @@ You should see a message like this:
|
||||
|
||||
copy this secret to your `.env.server` file under `STRIPE_WEBHOOK_SECRET=`.
|
||||
|
||||
## Testing Webhooks via the Stripe CLI
|
||||
### Testing Webhooks via the Stripe CLI
|
||||
|
||||
- In a new terminal window, run the following command:
|
||||
|
||||
@ -173,7 +173,7 @@ We've packed in a ton of features and love into this SaaS starter, and offer it
|
||||
If you're finding this template and its guides useful, consider giving us [a star on GitHub](https://github.com/wasp-lang/wasp)
|
||||
:::
|
||||
|
||||
## Testing Checkout and Payments via the Client
|
||||
### Testing Checkout and Payments 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:
|
||||
@ -199,7 +199,7 @@ wasp db studio
|
||||
If you want to learn more about how a user's payment status, subscription status, and subscription tier affect a user's priveledges within the app, check out the [User Overview](/general/user-overview) reference.
|
||||
:::
|
||||
|
||||
# Lemon Squeezy
|
||||
## Lemon Squeezy
|
||||
|
||||
First, make sure you've defined your payment processor in `src/payment/paymentProcessor.ts`, as described in the [important first steps](#important-first-steps).
|
||||
|
||||
@ -211,7 +211,7 @@ We've packed in a ton of features and love into this SaaS starter, and offer it
|
||||
If you're finding this template and its guides useful, consider giving us [a star on GitHub](https://github.com/wasp-lang/wasp)
|
||||
:::
|
||||
|
||||
## Get your test Lemon Squeezy API Keys
|
||||
### Get your test Lemon Squeezy 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://app.lemonsqueezy.com/settings/api](https://app.lemonsqueezy.com/settings/api) and creating a new API key.
|
||||
|
||||
@ -219,7 +219,7 @@ Once you've created your account, you'll need to get your test API keys. You can
|
||||
- Give your API key a name
|
||||
- Copy and paste it in your `.env.server` file under `LEMONSQUEEZY_API_KEY=`
|
||||
|
||||
## Create Test Products
|
||||
### Create Test Products
|
||||
|
||||
To create a test product, go to the test products url [https://app.lemonsqueezy.com/products](https://app.lemonsqueezy.com/products).
|
||||
|
||||
@ -239,7 +239,7 @@ To create a test product, go to the test products url [https://app.lemonsqueezy.
|
||||
- As well as a one-time payment product/credits-based environment variable, `PAYMENTS_CREDITS__10_PLAN_ID=`.
|
||||
- Note that if you change the names of the these environment variables, you'll need to update your app code to match these names as well.
|
||||
|
||||
## Create and Use the Lemon Squeezy Webhook in Local Development
|
||||
### Create and Use the Lemon Squeezy Webhook in Local Development
|
||||
|
||||
Lemon Squeezy sends messages/updates to your Wasp app via its webhook, e.g. when a payment is successful. For that to work during development, we need to expose our locally running (via `wasp start`) Wasp app and make it available online, specifically the server part of it. Since the Wasp server runs on port 3001, you should run ngrok on port 3001, which will provide you with a public URL that you can use to configure Lemon Squeezy with.
|
||||
|
||||
@ -272,6 +272,6 @@ Now go to your [Lemon Squeezy Webhooks Dashboard](https://app.lemonsqueezy.com/s
|
||||
|
||||
You're now ready to start consuming Lemon Squeezy webhook events in local development.
|
||||
|
||||
# Deploying
|
||||
## Deploying
|
||||
|
||||
Once you deploy your app, you can follow the same steps, just make sure that you are no longer in test mode within the Stripe or Lemon Squeezy Dashboards. After you've repeated the steps in live mode, add the new API keys and price/variant IDs to your environment variables in your deployed environment.
|
||||
|
@ -38,16 +38,15 @@ export const paymentPlanCards: Record<PaymentPlanId, PaymentPlanCard> = {
|
||||
|
||||
const PricingPage = () => {
|
||||
const [isPaymentLoading, setIsPaymentLoading] = useState<boolean>(false);
|
||||
|
||||
const { data: user, isLoading: isUserLoading } = useAuth();
|
||||
|
||||
const shouldFetchCustomerPortalUrl = !!user && !!user.subscriptionStatus;
|
||||
|
||||
const { data: user } = useAuth();
|
||||
const isUserSubscribed = !!user && !!user.subscriptionStatus && user.subscriptionStatus !== 'deleted';
|
||||
|
||||
const {
|
||||
data: customerPortalUrl,
|
||||
isLoading: isCustomerPortalUrlLoading,
|
||||
error: customerPortalUrlError,
|
||||
} = useQuery(getCustomerPortalUrl, { enabled: shouldFetchCustomerPortalUrl });
|
||||
} = useQuery(getCustomerPortalUrl, { enabled: isUserSubscribed });
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
@ -153,7 +152,7 @@ const PricingPage = () => {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{!!user && !!user.subscriptionStatus ? (
|
||||
{isUserSubscribed ? (
|
||||
<button
|
||||
onClick={handleCustomerPortalClick}
|
||||
disabled={isCustomerPortalUrlLoading}
|
||||
|
@ -59,16 +59,6 @@ export const stripeMiddlewareConfigFn: MiddlewareConfigFn = (middlewareConfig) =
|
||||
return middlewareConfig;
|
||||
};
|
||||
|
||||
const LineItemsPriceSchema = z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
price: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export async function handleCheckoutSessionCompleted(
|
||||
session: Stripe.Checkout.Session,
|
||||
prismaUserDelegate: PrismaClient["user"]
|
||||
@ -77,21 +67,10 @@ export async function handleCheckoutSessionCompleted(
|
||||
const { line_items } = await stripe.checkout.sessions.retrieve(session.id, {
|
||||
expand: ['line_items'],
|
||||
});
|
||||
const result = LineItemsPriceSchema.safeParse(line_items);
|
||||
if (!result.success) {
|
||||
throw new HttpError(400, 'No price id in line item');
|
||||
}
|
||||
if (result.data.data.length > 1) {
|
||||
throw new HttpError(400, 'More than one line item in session');
|
||||
}
|
||||
const lineItemPriceId = result.data.data[0].price.id;
|
||||
|
||||
const planId = Object.values(PaymentPlanId).find(
|
||||
(planId) => paymentPlans[planId].getPaymentProcessorPlanId() === lineItemPriceId
|
||||
);
|
||||
if (!planId) {
|
||||
throw new Error(`No plan with stripe price id ${lineItemPriceId}`);
|
||||
}
|
||||
const lineItemPriceId = extractPriceId(line_items);
|
||||
|
||||
const planId = getPlanIdByPriceId(lineItemPriceId);
|
||||
const plan = paymentPlans[planId];
|
||||
|
||||
let subscriptionPlan: PaymentPlanId | undefined;
|
||||
@ -126,6 +105,9 @@ export async function handleCustomerSubscriptionUpdated(
|
||||
const userStripeId = validateUserStripeIdOrThrow(subscription.customer);
|
||||
let subscriptionStatus: SubscriptionStatus | undefined;
|
||||
|
||||
const priceId = extractPriceId(subscription.items);
|
||||
const subscriptionPlan = getPlanIdByPriceId(priceId);
|
||||
|
||||
// There are other subscription statuses, such as `trialing` that we are not handling and simply ignore
|
||||
// If you'd like to handle more statuses, you can add more cases above. Make sure to update the `SubscriptionStatus` type in `payment/plans.ts` as well
|
||||
if (subscription.status === 'active') {
|
||||
@ -134,7 +116,7 @@ export async function handleCustomerSubscriptionUpdated(
|
||||
subscriptionStatus = 'past_due';
|
||||
}
|
||||
if (subscriptionStatus) {
|
||||
const user = await updateUserStripePaymentDetails({ userStripeId, subscriptionStatus }, prismaUserDelegate);
|
||||
const user = await updateUserStripePaymentDetails({ userStripeId, subscriptionPlan, subscriptionStatus }, prismaUserDelegate);
|
||||
if (subscription.cancel_at_period_end) {
|
||||
if (user.email) {
|
||||
await emailSender.send({
|
||||
@ -162,3 +144,34 @@ function validateUserStripeIdOrThrow(userStripeId: Stripe.Checkout.Session['cust
|
||||
if (typeof userStripeId !== 'string') throw new HttpError(400, 'Customer id is not a string');
|
||||
return userStripeId;
|
||||
}
|
||||
|
||||
const LineItemsPriceSchema = z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
price: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
function extractPriceId(items: Stripe.Checkout.Session['line_items'] | Stripe.Subscription['items']) {
|
||||
const result = LineItemsPriceSchema.safeParse(items);
|
||||
if (!result.success) {
|
||||
throw new HttpError(400, 'No price id in stripe event object');
|
||||
}
|
||||
if (result.data.data.length > 1) {
|
||||
throw new HttpError(400, 'More than one item in stripe event object');
|
||||
}
|
||||
return result.data.data[0].price.id;
|
||||
}
|
||||
|
||||
function getPlanIdByPriceId(priceId: string): PaymentPlanId {
|
||||
const planId = Object.values(PaymentPlanId).find(
|
||||
(planId) => paymentPlans[planId].getPaymentProcessorPlanId() === priceId
|
||||
);
|
||||
if (!planId) {
|
||||
throw new Error(`No plan with Stripe price id ${priceId}`);
|
||||
}
|
||||
return planId;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user