Skip to content

Commit 4763cac

Browse files
committed
feat(payments-demo): implement payments demo page and related API endpoints
- Added a new Payments Demo page to showcase payment functionalities, including product listing and checkout options. - Implemented API endpoints for creating checkout URLs, sending test emails, and validating configuration overrides. - Enhanced user experience with metrics display for email usage and product subscriptions. - Updated the header component to include a link to the Payments Demo page.
1 parent 4a6b7b4 commit 4763cac

5 files changed

Lines changed: 406 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { branchConfigSchema, getConfigOverrideErrors } from "@stackframe/stack-shared/dist/config/schema";
2+
import { ITEM_IDS, PLAN_LIMITS } from "@stackframe/stack-shared/dist/plans";
3+
import { NextResponse } from "next/server";
4+
import { stackServerApp } from "src/stack";
5+
6+
function readValidationResult(result: Awaited<ReturnType<typeof getConfigOverrideErrors>>) {
7+
if (result.status === "ok") {
8+
return {
9+
accepted: true,
10+
error: null,
11+
};
12+
}
13+
return {
14+
accepted: false,
15+
error: result.error,
16+
};
17+
}
18+
19+
export async function GET() {
20+
const project = await stackServerApp.getProject();
21+
const includeByDefaultValidation = await getConfigOverrideErrors(branchConfigSchema, {
22+
"payments.products.paymentsDemoInvalidFree.prices": "include-by-default",
23+
});
24+
25+
return NextResponse.json({
26+
projectId: project.id,
27+
includeByDefaultValidation: readValidationResult(includeByDefaultValidation),
28+
expected: {
29+
freePrice: "0.00",
30+
freeInterval: [1, "month"],
31+
freeEmailsPerMonth: PLAN_LIMITS.free.emailsPerMonth,
32+
emailItemId: ITEM_IDS.emailsPerMonth,
33+
emailsPerMonthRepeat: [1, "month"],
34+
},
35+
});
36+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { NextResponse } from "next/server";
2+
import { stackServerApp } from "src/stack";
3+
4+
function isRecord(value: unknown): value is Record<string, unknown> {
5+
return typeof value === "object" && value !== null && !Array.isArray(value);
6+
}
7+
8+
function readBody(value: unknown): { teamId: string, productId: "team" | "growth", returnUrl?: string } {
9+
if (!isRecord(value)) {
10+
throw new Error("Request body must be an object.");
11+
}
12+
13+
const { teamId, productId, returnUrl } = value;
14+
if (typeof teamId !== "string" || teamId === "") {
15+
throw new Error("teamId is required.");
16+
}
17+
if (productId !== "team" && productId !== "growth") {
18+
throw new Error("productId must be team or growth.");
19+
}
20+
if (returnUrl !== undefined && typeof returnUrl !== "string") {
21+
throw new Error("returnUrl must be a string.");
22+
}
23+
24+
return { teamId, productId, returnUrl };
25+
}
26+
27+
export async function POST(request: Request) {
28+
const user = await stackServerApp.getUser();
29+
if (user == null) {
30+
return NextResponse.json({ error: "Sign in before creating a checkout URL." }, { status: 401 });
31+
}
32+
const body = readBody(await request.json());
33+
const teams = await user.listTeams();
34+
const team = teams.find((candidate) => candidate.id === body.teamId);
35+
if (team == null) {
36+
return NextResponse.json({ error: "Current user is not a member of that team." }, { status: 403 });
37+
}
38+
39+
const url = await team.createCheckoutUrl({
40+
productId: body.productId,
41+
returnUrl: body.returnUrl,
42+
});
43+
44+
return NextResponse.json({ url });
45+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { NextResponse } from "next/server";
2+
import { stackServerApp } from "src/stack";
3+
4+
function isRecord(value: unknown): value is Record<string, unknown> {
5+
return typeof value === "object" && value !== null && !Array.isArray(value);
6+
}
7+
8+
function readCount(value: unknown): number {
9+
if (!isRecord(value)) {
10+
return 1;
11+
}
12+
13+
const count = value.count;
14+
if (count === undefined) {
15+
return 1;
16+
}
17+
if (typeof count !== "number" || !Number.isInteger(count) || count < 1 || count > 10) {
18+
throw new Error("count must be an integer between 1 and 10.");
19+
}
20+
return count;
21+
}
22+
23+
export async function POST(request: Request) {
24+
const user = await stackServerApp.getUser({ or: "throw" });
25+
const body: unknown = await request.json();
26+
const count = readCount(body);
27+
28+
for (let i = 0; i < count; i++) {
29+
await stackServerApp.sendEmail({
30+
userIds: [user.id],
31+
subject: `Payments demo quota test ${i + 1}/${count}`,
32+
html: `<p>Payments demo quota test email ${i + 1} of ${count}.</p>`,
33+
});
34+
}
35+
36+
return NextResponse.json({
37+
sent: count,
38+
userId: user.id,
39+
});
40+
}

0 commit comments

Comments
 (0)