mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-04-10 21:09:04 +02:00
Merge branch 'main' into miho-webhooks-runtime-validations
This commit is contained in:
commit
a969544793
@ -1,14 +1,15 @@
|
||||
--- template/app/src/client/components/NavBar/NavBar.tsx
|
||||
+++ opensaas-sh/app/src/client/components/NavBar/NavBar.tsx
|
||||
@@ -32,6 +32,7 @@
|
||||
@@ -32,7 +32,7 @@
|
||||
!isLandingPage,
|
||||
})}
|
||||
>
|
||||
- {isLandingPage && <Announcement />}
|
||||
+ {/* {isLandingPage && <Announcement />} */}
|
||||
<nav className='flex items-center justify-between p-6 lg:px-8' aria-label='Global'>
|
||||
<div className='flex items-center lg:flex-1'>
|
||||
<WaspRouterLink
|
||||
@@ -39,9 +40,7 @@
|
||||
@@ -40,9 +40,7 @@
|
||||
className='flex items-center -m-1.5 p-1.5 text-gray-900 duration-300 ease-in-out hover:text-yellow-500'
|
||||
>
|
||||
<NavLogo />
|
||||
@ -19,7 +20,7 @@
|
||||
</WaspRouterLink>
|
||||
</div>
|
||||
<div className='flex lg:hidden'>
|
||||
@@ -56,13 +55,13 @@
|
||||
@@ -57,13 +55,13 @@
|
||||
</div>
|
||||
<div className='hidden lg:flex lg:gap-x-12'>{renderNavigationItems(navigationItems)}</div>
|
||||
<div className='hidden lg:flex lg:flex-1 gap-3 justify-end items-center'>
|
||||
@ -36,7 +37,7 @@
|
||||
</div>
|
||||
</WaspRouterLink>
|
||||
) : (
|
||||
@@ -77,7 +76,7 @@
|
||||
@@ -78,7 +76,7 @@
|
||||
<Dialog.Panel className='fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white dark:text-white dark:bg-boxdark px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<WaspRouterLink to={routes.LandingPageRoute.to} className='-m-1.5 p-1.5'>
|
||||
@ -45,7 +46,7 @@
|
||||
<NavLogo />
|
||||
</WaspRouterLink>
|
||||
<button
|
||||
@@ -96,7 +95,7 @@
|
||||
@@ -97,7 +95,7 @@
|
||||
{isUserLoading ? null : !user ? (
|
||||
<WaspRouterLink to={routes.LoginRoute.to}>
|
||||
<div className='flex justify-end items-center duration-300 ease-in-out text-gray-900 hover:text-yellow-500 dark:text-white'>
|
||||
@ -54,32 +55,41 @@
|
||||
</div>
|
||||
</WaspRouterLink>
|
||||
) : (
|
||||
@@ -138,3 +137,27 @@
|
||||
);
|
||||
@@ -140,30 +138,26 @@
|
||||
});
|
||||
}
|
||||
+
|
||||
|
||||
-const ContestURL = 'https://github.com/wasp-lang/wasp';
|
||||
+const ContestURL =
|
||||
+ 'https://docs.opensaas.sh/blog/';
|
||||
+
|
||||
+function Announcement() {
|
||||
+ return (
|
||||
+ <div className='flex justify-center items-center gap-3 p-3 w-full bg-gradient-to-r from-[#d946ef] to-[#fc0] font-semibold text-white text-center z-49'>
|
||||
|
||||
function Announcement() {
|
||||
return (
|
||||
<div className='flex justify-center items-center gap-3 p-3 w-full bg-gradient-to-r from-[#d946ef] to-[#fc0] font-semibold text-white text-center z-49'>
|
||||
- <p
|
||||
- onClick={() => window.open(ContestURL, '_blank')}
|
||||
- className='hidden lg:block cursor-pointer hover:opacity-90 hover:drop-shadow'
|
||||
- >
|
||||
- Support Open-Source Software!
|
||||
- </p>
|
||||
+ <p onClick={() => window.open(ContestURL, '_blank')} className='hidden lg:block cursor-pointer hover:opacity-90 hover:drop-shadow'>🍪 THE MOST ANNOYING COOKIE BANNER EVER HACKATHON 🤬</p>
|
||||
+ <div className='hidden lg:block self-stretch w-0.5 bg-white'></div>
|
||||
+ <div
|
||||
+ onClick={() => window.open(ContestURL, '_blank')}
|
||||
+ className='hidden lg:block cursor-pointer rounded-full bg-neutral-700 px-2.5 py-1 text-xs hover:bg-neutral-600 tracking-wider'
|
||||
+ >
|
||||
<div className='hidden lg:block self-stretch w-0.5 bg-white'></div>
|
||||
<div
|
||||
onClick={() => window.open(ContestURL, '_blank')}
|
||||
className='hidden lg:block cursor-pointer rounded-full bg-neutral-700 px-2.5 py-1 text-xs hover:bg-neutral-600 tracking-wider'
|
||||
>
|
||||
- Star Our Repo on Github ⭐️ →
|
||||
+ Enter here and win prizes! →
|
||||
+ </div>
|
||||
+ <div
|
||||
+ onClick={() => window.open(ContestURL, '_blank')}
|
||||
+ className='lg:hidden cursor-pointer rounded-full bg-neutral-700 px-2.5 py-1 text-xs hover:bg-neutral-600 tracking-wider'
|
||||
+ >
|
||||
</div>
|
||||
<div
|
||||
onClick={() => window.open(ContestURL, '_blank')}
|
||||
className='lg:hidden cursor-pointer rounded-full bg-neutral-700 px-2.5 py-1 text-xs hover:bg-neutral-600 tracking-wider'
|
||||
>
|
||||
- ⭐️ Star the Our Repo on Github and Support Open-Source! ⭐️
|
||||
+ 🍪 The Most Annoying Cookie Banner Contest! 🤬 →
|
||||
+ </div>
|
||||
+ </div>
|
||||
+ );
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
-}
|
||||
+}
|
||||
\ No newline at end of file
|
||||
|
@ -1,6 +1,6 @@
|
||||
--- template/app/src/payment/stripe/paymentProcessor.ts
|
||||
+++ opensaas-sh/app/src/payment/stripe/paymentProcessor.ts
|
||||
@@ -21,7 +21,7 @@
|
||||
@@ -20,7 +20,7 @@
|
||||
id: userId
|
||||
},
|
||||
data: {
|
||||
|
@ -178,6 +178,7 @@ export const stripe = new Stripe(process.env.STRIPE_KEY!, {
|
||||
<br/>- `customer.subscription.deleted`
|
||||
<br/>- `customer.subscription.updated`
|
||||
<br/>- `invoice.paid`
|
||||
<br/>- `payment_intent.succeeded`
|
||||
<Image src={stripeSigningSecret} alt="signing secret" loading="lazy" />
|
||||
5. after that, go to the webhook you just created and `reveal` the new signing secret.
|
||||
6. add this secret to your deployed server's `STRIPE_WEBHOOK_SECRET=` environment variable. <br/>If you've deployed to Fly.io, you can do that easily with the following command:
|
||||
|
@ -32,6 +32,7 @@ export default function AppNavBar({ navigationItems }: { navigationItems: Naviga
|
||||
!isLandingPage,
|
||||
})}
|
||||
>
|
||||
{isLandingPage && <Announcement />}
|
||||
<nav className='flex items-center justify-between p-6 lg:px-8' aria-label='Global'>
|
||||
<div className='flex items-center lg:flex-1'>
|
||||
<WaspRouterLink
|
||||
@ -138,3 +139,31 @@ function renderNavigationItems(
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const ContestURL = 'https://github.com/wasp-lang/wasp';
|
||||
|
||||
function Announcement() {
|
||||
return (
|
||||
<div className='flex justify-center items-center gap-3 p-3 w-full bg-gradient-to-r from-[#d946ef] to-[#fc0] font-semibold text-white text-center z-49'>
|
||||
<p
|
||||
onClick={() => window.open(ContestURL, '_blank')}
|
||||
className='hidden lg:block cursor-pointer hover:opacity-90 hover:drop-shadow'
|
||||
>
|
||||
Support Open-Source Software!
|
||||
</p>
|
||||
<div className='hidden lg:block self-stretch w-0.5 bg-white'></div>
|
||||
<div
|
||||
onClick={() => window.open(ContestURL, '_blank')}
|
||||
className='hidden lg:block cursor-pointer rounded-full bg-neutral-700 px-2.5 py-1 text-xs hover:bg-neutral-600 tracking-wider'
|
||||
>
|
||||
Star Our Repo on Github ⭐️ →
|
||||
</div>
|
||||
<div
|
||||
onClick={() => window.open(ContestURL, '_blank')}
|
||||
className='lg:hidden cursor-pointer rounded-full bg-neutral-700 px-2.5 py-1 text-xs hover:bg-neutral-600 tracking-wider'
|
||||
>
|
||||
⭐️ Star the Our Repo on Github and Support Open-Source! ⭐️
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import type { StripeMode } from './paymentProcessor';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
import { stripe } from './stripeClient';
|
||||
import { assertUnreachable } from '../../shared/utils';
|
||||
|
||||
// WASP_WEB_CLIENT_URL will be set up by Wasp when deploying to production: https://wasp.sh/docs/deploying
|
||||
const DOMAIN = process.env.WASP_WEB_CLIENT_URL || 'http://localhost:3000';
|
||||
@ -27,8 +29,20 @@ export async function fetchStripeCustomer(customerEmail: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function createStripeCheckoutSession({ userId, priceId, customerId, mode }: { userId: string, priceId: string; customerId: string; mode: StripeMode }) {
|
||||
interface CreateStripeCheckoutSessionParams {
|
||||
priceId: string;
|
||||
customerId: string;
|
||||
mode: StripeMode;
|
||||
}
|
||||
|
||||
export async function createStripeCheckoutSession({
|
||||
priceId,
|
||||
customerId,
|
||||
mode,
|
||||
}: CreateStripeCheckoutSessionParams) {
|
||||
try {
|
||||
const paymentIntentData = getPaymentIntentData({ mode, priceId });
|
||||
|
||||
return await stripe.checkout.sessions.create({
|
||||
line_items: [
|
||||
{
|
||||
@ -44,9 +58,29 @@ export async function createStripeCheckoutSession({ userId, priceId, customerId,
|
||||
address: 'auto',
|
||||
},
|
||||
customer: customerId,
|
||||
// Stripe only allows us to pass payment intent metadata for one-time payments, not subscriptions.
|
||||
// We do this so that we can capture priceId in the payment_intent.succeeded webhook
|
||||
// and easily confirm the user's payment based on the price id. For subscriptions, we can get the price id
|
||||
// in the customer.subscription.updated webhook via the line_items field.
|
||||
payment_intent_data: paymentIntentData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function getPaymentIntentData({ mode, priceId }: { mode: StripeMode; priceId: string }):
|
||||
| {
|
||||
metadata: { priceId: string };
|
||||
}
|
||||
| undefined {
|
||||
switch (mode) {
|
||||
case 'subscription':
|
||||
return undefined;
|
||||
case 'payment':
|
||||
return { metadata: { priceId } };
|
||||
default:
|
||||
assertUnreachable(mode);
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ export const stripePaymentProcessor: PaymentProcessor = {
|
||||
createCheckoutSession: async ({ userId, userEmail, paymentPlan, prismaUserDelegate }: CreateCheckoutSessionArgs) => {
|
||||
const customer = await fetchStripeCustomer(userEmail);
|
||||
const stripeSession = await createStripeCheckoutSession({
|
||||
userId,
|
||||
priceId: paymentPlan.getPaymentProcessorPlanId(),
|
||||
customerId: customer.id,
|
||||
mode: paymentPlanEffectToStripeMode(paymentPlan.effect),
|
||||
|
@ -4,7 +4,7 @@ import { type PrismaClient } from '@prisma/client';
|
||||
import express from 'express';
|
||||
import { Stripe } from 'stripe';
|
||||
import { stripe } from './stripeClient';
|
||||
import { paymentPlans, PaymentPlanId, SubscriptionStatus } from '../plans';
|
||||
import { paymentPlans, PaymentPlanId, SubscriptionStatus, PaymentPlanEffect, PaymentPlan } from '../plans';
|
||||
import { updateUserStripePaymentDetails } from './paymentDetails';
|
||||
import { emailSender } from 'wasp/server/email';
|
||||
import { assertUnreachable } from '../../shared/utils';
|
||||
@ -13,6 +13,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
InvoicePaidData,
|
||||
parseWebhookPayload,
|
||||
PaymentIntentSucceededData,
|
||||
SessionCompletedData,
|
||||
SubscriptionDeletedData,
|
||||
SubscriptionUpdatedData,
|
||||
@ -43,6 +44,9 @@ export const stripeWebhook: PaymentsWebhook = async (request, response, context)
|
||||
case 'invoice.paid':
|
||||
await handleInvoicePaid(payload.data, prismaUserDelegate);
|
||||
break;
|
||||
case 'payment_intent.succeeded':
|
||||
await handlePaymentIntentSucceeded(payload.data, prismaUserDelegate);
|
||||
break;
|
||||
case 'customer.subscription.updated':
|
||||
await handleCustomerSubscriptionUpdated(payload.data, prismaUserDelegate);
|
||||
break;
|
||||
@ -87,6 +91,9 @@ export const stripeMiddlewareConfigFn: MiddlewareConfigFn = (middlewareConfig) =
|
||||
return middlewareConfig;
|
||||
};
|
||||
|
||||
// Because a checkout session completed could potentially result in a failed payment,
|
||||
// we can update the user's payment details here, but confirm credits or a subscription
|
||||
// if the payment succeeds in other, more specific, webhooks.
|
||||
export async function handleCheckoutSessionCompleted(
|
||||
session: SessionCompletedData,
|
||||
prismaUserDelegate: PrismaClient['user']
|
||||
@ -100,32 +107,57 @@ export async function handleCheckoutSessionCompleted(
|
||||
|
||||
const planId = getPlanIdByPriceId(lineItemPriceId);
|
||||
const plan = paymentPlans[planId];
|
||||
|
||||
let subscriptionPlan: PaymentPlanId | undefined;
|
||||
let numOfCreditsPurchased: number | undefined;
|
||||
switch (plan.effect.kind) {
|
||||
case 'subscription':
|
||||
subscriptionPlan = planId;
|
||||
break;
|
||||
case 'credits':
|
||||
numOfCreditsPurchased = plan.effect.amount;
|
||||
break;
|
||||
default:
|
||||
assertUnreachable(plan.effect);
|
||||
if (plan.effect.kind === 'credits') {
|
||||
return;
|
||||
}
|
||||
const { subscriptionPlan } = getPlanEffectPaymentDetails({ planId, planEffect: plan.effect });
|
||||
|
||||
return updateUserStripePaymentDetails(
|
||||
{ userStripeId, subscriptionPlan, numOfCreditsPurchased, datePaid: new Date() },
|
||||
prismaUserDelegate
|
||||
);
|
||||
return updateUserStripePaymentDetails({ userStripeId, subscriptionPlan }, prismaUserDelegate);
|
||||
}
|
||||
|
||||
// This is called when a subscription is purchased or renewed and payment succeeds.
|
||||
// Invoices are not created for one-time payments, so we handle them in the payment_intent.succeeded webhook.
|
||||
export async function handleInvoicePaid(invoice: InvoicePaidData, prismaUserDelegate: PrismaClient['user']) {
|
||||
const userStripeId = validateUserStripeIdOrThrow(invoice.customer);
|
||||
const datePaid = new Date(invoice.period_start * 1000);
|
||||
return updateUserStripePaymentDetails({ userStripeId, datePaid }, prismaUserDelegate);
|
||||
}
|
||||
|
||||
export async function handlePaymentIntentSucceeded(
|
||||
paymentIntent: PaymentIntentSucceededData,
|
||||
prismaUserDelegate: PrismaClient['user']
|
||||
) {
|
||||
// We handle invoices in the invoice.paid webhook. Invoices exist for subscription payments,
|
||||
// but not for one-time payment/credits products which use the Stripe `payment` mode on checkout sessions.
|
||||
if (paymentIntent.invoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userStripeId = validateUserStripeIdOrThrow(paymentIntent.customer);
|
||||
const datePaid = new Date(paymentIntent.created * 1000);
|
||||
|
||||
// We capture the price id from the payment intent metadata
|
||||
// that we passed in when creating the checkout session in checkoutUtils.ts.
|
||||
const { metadata } = paymentIntent;
|
||||
|
||||
if (!metadata.priceId) {
|
||||
throw new HttpError(400, 'No price id found in payment intent');
|
||||
}
|
||||
|
||||
const planId = getPlanIdByPriceId(metadata.priceId);
|
||||
const plan = paymentPlans[planId];
|
||||
if (plan.effect.kind === 'subscription') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { numOfCreditsPurchased } = getPlanEffectPaymentDetails({ planId, planEffect: plan.effect });
|
||||
|
||||
return updateUserStripePaymentDetails(
|
||||
{ userStripeId, numOfCreditsPurchased, datePaid },
|
||||
prismaUserDelegate
|
||||
);
|
||||
}
|
||||
|
||||
export async function handleCustomerSubscriptionUpdated(
|
||||
subscription: SubscriptionUpdatedData,
|
||||
prismaUserDelegate: PrismaClient['user']
|
||||
@ -217,3 +249,23 @@ function getPlanIdByPriceId(priceId: string): PaymentPlanId {
|
||||
}
|
||||
return planId;
|
||||
}
|
||||
|
||||
function getPlanEffectPaymentDetails({
|
||||
planId,
|
||||
planEffect,
|
||||
}: {
|
||||
planId: PaymentPlanId;
|
||||
planEffect: PaymentPlanEffect;
|
||||
}): {
|
||||
subscriptionPlan: PaymentPlanId | undefined;
|
||||
numOfCreditsPurchased: number | undefined;
|
||||
} {
|
||||
switch (planEffect.kind) {
|
||||
case 'subscription':
|
||||
return { subscriptionPlan: planId, numOfCreditsPurchased: undefined };
|
||||
case 'credits':
|
||||
return { subscriptionPlan: undefined, numOfCreditsPurchased: planEffect.amount };
|
||||
default:
|
||||
assertUnreachable(planEffect);
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,14 @@ export async function parseWebhookPayload(rawStripeEvent: Stripe.Event) {
|
||||
throw new Error('Error parsing Stripe event object');
|
||||
});
|
||||
return { eventName: event.type, data: invoice };
|
||||
case 'payment_intent.succeeded':
|
||||
const paymentIntent = await paymentIntentSucceededDataSchema
|
||||
.parseAsync(event.data.object)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
throw new Error('Error parsing Stripe event object');
|
||||
});
|
||||
return { eventName: event.type, data: paymentIntent };
|
||||
case 'customer.subscription.updated':
|
||||
const updatedSubscription = await subscriptionUpdatedDataSchema
|
||||
.parseAsync(event.data.object)
|
||||
@ -61,6 +69,16 @@ const invoicePaidDataSchema = z.object({
|
||||
period_start: z.number(),
|
||||
});
|
||||
|
||||
// This is a subtype of Stripe.PaymentIntent from "stripe"
|
||||
const paymentIntentSucceededDataSchema = z.object({
|
||||
invoice: z.unknown().optional(),
|
||||
created: z.number(),
|
||||
metadata: z.object({
|
||||
priceId: z.string(),
|
||||
}),
|
||||
customer: z.string(),
|
||||
});
|
||||
|
||||
// This is a subtype of Stripe.Subscription from "stripe"
|
||||
const subscriptionUpdatedDataSchema = z.object({
|
||||
customer: z.string(),
|
||||
@ -86,6 +104,8 @@ export type SessionCompletedData = z.infer<typeof sessionCompletedDataSchema>;
|
||||
|
||||
export type InvoicePaidData = z.infer<typeof invoicePaidDataSchema>;
|
||||
|
||||
export type PaymentIntentSucceededData = z.infer<typeof paymentIntentSucceededDataSchema>;
|
||||
|
||||
export type SubscriptionUpdatedData = z.infer<typeof subscriptionUpdatedDataSchema>;
|
||||
|
||||
export type SubscriptionDeletedData = z.infer<typeof subscriptionDeletedDataSchema>;
|
||||
|
@ -89,9 +89,9 @@ test('AI schedule generation fails on 4th attempt', async () => {
|
||||
expect(tableTextContent.includes(task2.toLowerCase())).toBeFalsy();
|
||||
});
|
||||
|
||||
test('Make test payment with Stripe', async () => {
|
||||
const PLAN_NAME = 'Hobby';
|
||||
await makeStripePayment({ test, page, planName: PLAN_NAME });
|
||||
test('Make test payment with Stripe for hobby plan', async () => {
|
||||
const planId = 'hobby';
|
||||
await makeStripePayment({ test, page, planId });
|
||||
});
|
||||
|
||||
test('User should be able to generate another schedule after payment', async () => {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { signUserUp, logUserIn, createRandomUser, makeStripePayment, type User } from './utils';
|
||||
import { signUserUp, logUserIn, createRandomUser, makeStripePayment, type User, acceptAllCookies } from './utils';
|
||||
|
||||
let page: Page;
|
||||
let testUser: User;
|
||||
|
||||
async function logNewUserIn() {
|
||||
async function createAndLogInNewUser() {
|
||||
testUser = createRandomUser();
|
||||
await signUserUp({ page: page, user: testUser });
|
||||
await logUserIn({ page: page, user: testUser });
|
||||
@ -33,9 +33,9 @@ test('User should see Log In to Buy Plan button', async () => {
|
||||
});
|
||||
|
||||
test('User should see the Buy Plan button before payment', async () => {
|
||||
// We only need to log the user in once since the tests are running sequentially
|
||||
// We only need to log the user in once since the tests are running sequentially
|
||||
// and the same page is being shared between all the tests.
|
||||
await logNewUserIn();
|
||||
await createAndLogInNewUser();
|
||||
await page.goto('/pricing');
|
||||
// There are three tiers on the page, so we want to retrieve the first of the three buttons
|
||||
const manageSubscriptionButton = page.getByRole('button', { name: 'Buy plan' }).first();
|
||||
@ -43,10 +43,10 @@ test('User should see the Buy Plan button before payment', async () => {
|
||||
await expect(manageSubscriptionButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test('Make test payment with Stripe', async () => {
|
||||
const PLAN_NAME = 'Hobby';
|
||||
test('Make test payment with Stripe for hobby plan', async () => {
|
||||
const planId = 'hobby';
|
||||
await page.goto('/');
|
||||
await makeStripePayment({ test, page, planName: PLAN_NAME });
|
||||
await makeStripePayment({ test, page, planId });
|
||||
});
|
||||
|
||||
test('User should see the Manage Subscription button after payment', async () => {
|
||||
@ -62,3 +62,11 @@ test('User should see the Manage Subscription button after payment', async () =>
|
||||
await newTab.waitForLoadState();
|
||||
await expect(newTab).toHaveURL(/^https:\/\/billing\.stripe\.com\//);
|
||||
});
|
||||
|
||||
test('Make test payment with Stripe for 10 credits', async () => {
|
||||
await createAndLogInNewUser();
|
||||
await acceptAllCookies(page); // Clear the cookie consent modal so it doesn't interfere with the payment
|
||||
const planId = 'credits10';
|
||||
await page.goto('/');
|
||||
await makeStripePayment({ test, page, planId });
|
||||
});
|
||||
|
@ -39,6 +39,20 @@ export const logUserIn = async ({ page, user }: { page: Page; user: User }) => {
|
||||
|
||||
export const signUserUp = async ({ page, user }: { page: Page; user: User }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await page.evaluate(() => {
|
||||
try {
|
||||
const sessionId = localStorage.getItem('wasp:sessionId');
|
||||
if (sessionId) {
|
||||
localStorage.removeItem('wasp:sessionId');
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
console.error('Failed to clear localStorage:', e);
|
||||
}
|
||||
});
|
||||
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
await page.getByRole('link', { name: 'Log in' }).click();
|
||||
|
||||
@ -62,13 +76,22 @@ export const createRandomUser = () => {
|
||||
return { email, password: DEFAULT_PASSWORD } as User;
|
||||
};
|
||||
|
||||
export const makeStripePayment = async ({ test, page, planName }: { test: any; page: Page; planName: string }) => {
|
||||
export const makeStripePayment = async ({
|
||||
test,
|
||||
page,
|
||||
planId,
|
||||
}: {
|
||||
test: any;
|
||||
page: Page;
|
||||
planId: 'hobby' | 'pro' | 'credits10';
|
||||
}) => {
|
||||
test.slow(); // Stripe payments take a long time to confirm and can cause tests to fail so we use a longer timeout
|
||||
|
||||
await page.click('text="Pricing"');
|
||||
await page.waitForURL('**/pricing');
|
||||
|
||||
const buyBtn = page.getByRole('button', { name: 'Buy plan' }).first(); // "Hobby Plan" is the first of three plans
|
||||
const buyBtn = page.locator(`button[aria-describedby="${planId}"]`);
|
||||
|
||||
await expect(buyBtn).toBeVisible();
|
||||
await expect(buyBtn).toBeEnabled();
|
||||
await buyBtn.click();
|
||||
@ -92,5 +115,14 @@ export const makeStripePayment = async ({ test, page, planName }: { test: any; p
|
||||
|
||||
await page.waitForURL('**/checkout?success=true');
|
||||
await page.waitForURL('**/account');
|
||||
await expect(page.getByText(planName)).toBeVisible();
|
||||
if (planId === 'credits10') {
|
||||
await expect(page.getByText('Credits remaining: 13')).toBeVisible();
|
||||
} else {
|
||||
await expect(page.getByText(planId)).toBeVisible();
|
||||
}
|
||||
};
|
||||
|
||||
export const acceptAllCookies = async (page: Page) => {
|
||||
await page.waitForSelector('button:has-text("Accept all")');
|
||||
await page.click('button:has-text("Accept all")');
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user