Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 76 additions & 96 deletions template/app/src/user/AccountPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "../components/ui/card";
import { Separator } from "../components/ui/separator";
import {
PaymentPlanId,
SubscriptionStatus,
parsePaymentPlanId,
prettyPaymentPlanName,
Expand Down Expand Up @@ -59,17 +60,28 @@ export default function AccountPage({ user }: { user: User }) {
<dt className="text-muted-foreground text-sm font-medium">
Your Plan
</dt>
<UserCurrentPaymentPlan
subscriptionStatus={
user.subscriptionStatus as SubscriptionStatus
}
<UserCurrentSubscriptionPlan
subscriptionPlan={user.subscriptionPlan}
subscriptionStatus={user.subscriptionStatus}
datePaid={user.datePaid}
credits={user.credits}
/>
</div>
</div>
<Separator />
<div className="px-6 py-4">
<div className="grid grid-cols-1 sm:grid-cols-3 sm:gap-4">
<dt className="text-muted-foreground text-sm font-medium">
Credits
</dt>
<dd className="text-foreground mt-1 text-sm sm:col-span-1 sm:mt-0">
{user.credits + " credits"}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{user.credits + " credits"}
{user.credits} credits

Copy link
Contributor Author

@FranjoMindek FranjoMindek Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This screws up the <dd> rendering. It makes it have 2 children instead of one.
Which screws up locator text search in playwright.
Actually wanted to have it that way. 😞

</dd>
<div className="ml-auto mt-4 sm:mt-0">
<BuyMoreButton subscriptionStatus={user.subscriptionStatus} />
</div>
</div>
</div>
<Separator />
<div className="px-6 py-4">
<div className="grid grid-cols-1 sm:grid-cols-3 sm:gap-4">
<dt className="text-muted-foreground text-sm font-medium">
Expand All @@ -87,129 +99,97 @@ export default function AccountPage({ user }: { user: User }) {
);
}

type UserCurrentPaymentPlanProps = {
subscriptionPlan: string | null;
subscriptionStatus: SubscriptionStatus | null;
datePaid: Date | null;
credits: number;
};

function UserCurrentPaymentPlan({
function UserCurrentSubscriptionPlan({
subscriptionPlan,
subscriptionStatus,
datePaid,
credits,
}: UserCurrentPaymentPlanProps) {
if (subscriptionStatus && subscriptionPlan && datePaid) {
return (
<>
<dd className="text-foreground mt-1 text-sm sm:col-span-1 sm:mt-0">
{getUserSubscriptionStatusDescription({
subscriptionPlan,
subscriptionStatus,
datePaid,
})}
</dd>
{subscriptionStatus !== SubscriptionStatus.Deleted ? (
<CustomerPortalButton />
) : (
<BuyMoreButton />
)}
</>
}: Pick<User, "subscriptionPlan" | "subscriptionStatus" | "datePaid">) {
let subscriptionPlanMessage = "Free Plan";
if (
subscriptionPlan !== null &&
subscriptionStatus !== null &&
datePaid !== null
) {
subscriptionPlanMessage = formatSubscriptionStatusMessage(
parsePaymentPlanId(subscriptionPlan),
datePaid,
subscriptionStatus as SubscriptionStatus,
);
}

return (
<>
<dd className="text-foreground mt-1 text-sm sm:col-span-1 sm:mt-0">
Credits remaining: {credits}
{subscriptionPlanMessage}
</dd>
<BuyMoreButton />
<div className="ml-auto mt-4 sm:mt-0">
<CustomerPortalButton />
</div>
</>
);
}

function getUserSubscriptionStatusDescription({
subscriptionPlan,
subscriptionStatus,
datePaid,
}: {
subscriptionPlan: string;
subscriptionStatus: SubscriptionStatus;
datePaid: Date;
}) {
const planName = prettyPaymentPlanName(parsePaymentPlanId(subscriptionPlan));
const endOfBillingPeriod = prettyPrintEndOfBillingPeriod(datePaid);
return prettyPrintStatus(planName, subscriptionStatus, endOfBillingPeriod);
}

function prettyPrintStatus(
planName: string,
function formatSubscriptionStatusMessage(
subscriptionPlan: PaymentPlanId,
datePaid: Date,
subscriptionStatus: SubscriptionStatus,
endOfBillingPeriod: string,
): string {
const paymentPlanName = prettyPaymentPlanName(subscriptionPlan);
const statusToMessage: Record<SubscriptionStatus, string> = {
active: `${planName}`,
past_due: `Payment for your ${planName} plan is past due! Please update your subscription payment information.`,
cancel_at_period_end: `Your ${planName} plan subscription has been canceled, but remains active until the end of the current billing period${endOfBillingPeriod}`,
active: `${paymentPlanName}`,
past_due: `Payment for your ${paymentPlanName} plan is past due! Please update your subscription payment information.`,
cancel_at_period_end: `Your ${paymentPlanName} plan subscription has been canceled, but remains active until the end of the current billing period: ${prettyPrintEndOfBillingPeriod(
datePaid,
)}`,
deleted: `Your previous subscription has been canceled and is no longer active.`,
};
if (Object.keys(statusToMessage).includes(subscriptionStatus)) {
return statusToMessage[subscriptionStatus];
} else {
throw new Error(`Invalid subscriptionStatus: ${subscriptionStatus}`);

if (!statusToMessage[subscriptionStatus]) {
throw new Error(`Invalid subscription status: ${subscriptionStatus}`);
}

return statusToMessage[subscriptionStatus];
}

function prettyPrintEndOfBillingPeriod(date: Date) {
const oneMonthFromNow = new Date(date);
oneMonthFromNow.setMonth(oneMonthFromNow.getMonth() + 1);
return ": " + oneMonthFromNow.toLocaleDateString();
return oneMonthFromNow.toLocaleDateString();
}

function BuyMoreButton() {
function CustomerPortalButton() {
const { data: customerPortalUrl, isLoading: isCustomerPortalUrlLoading } =
useQuery(getCustomerPortalUrl);

if (!customerPortalUrl) {
return null;
}

return (
<div className="ml-4 flex-shrink-0 sm:col-span-1 sm:mt-0">
<WaspRouterLink
to={routes.PricingPageRoute.to}
className="text-primary hover:text-primary/80 text-sm font-medium transition-colors duration-200"
>
Buy More/Upgrade
</WaspRouterLink>
</div>
<a href={customerPortalUrl} target="_blank" rel="noopener noreferrer">
<Button disabled={isCustomerPortalUrlLoading} variant="link">
Manage Payment Details
</Button>
</a>
);
}

function CustomerPortalButton() {
const {
data: customerPortalUrl,
isLoading: isCustomerPortalUrlLoading,
error: customerPortalUrlError,
} = useQuery(getCustomerPortalUrl);

const handleClick = () => {
if (customerPortalUrlError) {
console.error("Error fetching customer portal url");
}

if (customerPortalUrl) {
window.open(customerPortalUrl, "_blank");
} else {
console.error("Customer portal URL is not available");
}
};
function BuyMoreButton({
subscriptionStatus,
}: Pick<User, "subscriptionStatus">) {
if (
subscriptionStatus === SubscriptionStatus.Active ||
subscriptionStatus === SubscriptionStatus.CancelAtPeriodEnd
) {
return null;
}

return (
<div className="ml-4 flex-shrink-0 sm:col-span-1 sm:mt-0">
<Button
onClick={handleClick}
disabled={isCustomerPortalUrlLoading}
variant="outline"
size="sm"
className="text-sm font-medium"
>
Manage Subscription
</Button>
</div>
<WaspRouterLink
to={routes.PricingPageRoute.to}
className="text-primary hover:text-primary/80 text-sm font-medium transition-colors duration-200"
>
<Button variant="link">Buy More Credits</Button>
</WaspRouterLink>
);
}
2 changes: 1 addition & 1 deletion template/e2e-tests/tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export const makeStripePayment = async ({
await page.waitForURL("**/checkout?status=success");
await page.waitForURL("**/account");
if (planId === "credits10") {
await expect(page.getByText("Credits remaining: 13")).toBeVisible();
await expect(page.getByText("13 credits")).toBeVisible();
} else {
await expect(page.getByText(planId)).toBeVisible();
}
Expand Down