Skip to content

Commit c3b6853

Browse files
authored
feat: add button to get more cshots (#19)
1 parent 1574b4b commit c3b6853

File tree

12 files changed

+271
-29
lines changed

12 files changed

+271
-29
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- CreateTable
2+
CREATE TABLE "payments" (
3+
"id" TEXT NOT NULL,
4+
"type" TEXT NOT NULL,
5+
"status" TEXT NOT NULL,
6+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
7+
"projectId" TEXT,
8+
9+
CONSTRAINT "payments_pkey" PRIMARY KEY ("id")
10+
);
11+
12+
-- AddForeignKey
13+
ALTER TABLE "payments" ADD CONSTRAINT "payments_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
Warnings:
3+
4+
- Added the required column `stripeSessionId` to the `payments` table without a default value. This is not possible if the table is not empty.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE "payments" ADD COLUMN "stripeSessionId" TEXT NOT NULL;

prisma/schema.prisma

+18-5
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ model VerificationToken {
6060
}
6161

6262
model Project {
63-
id String @id @default(cuid())
63+
id String @id @default(cuid())
6464
name String
6565
replicateModelId String?
6666
stripePaymentId String?
@@ -70,12 +70,13 @@ model Project {
7070
instanceClass String
7171
imageUrls String[]
7272
zipImageUrl String?
73-
createdAt DateTime @default(now())
74-
updatedAt DateTime @updatedAt
75-
User User? @relation(fields: [userId], references: [id])
73+
createdAt DateTime @default(now())
74+
updatedAt DateTime @updatedAt
75+
User User? @relation(fields: [userId], references: [id])
7676
userId String?
7777
shots Shot[]
78-
credits Int @default(100)
78+
credits Int @default(100)
79+
Payment Payment[]
7980
}
8081

8182
model Shot {
@@ -91,3 +92,15 @@ model Shot {
9192
bookmarked Boolean? @default(false)
9293
blurhash String?
9394
}
95+
96+
model Payment {
97+
id String @id @default(cuid())
98+
type String
99+
status String
100+
stripeSessionId String
101+
createdAt DateTime @default(now())
102+
Project Project? @relation(fields: [projectId], references: [id])
103+
projectId String?
104+
105+
@@map("payments")
106+
}

src/components/layout/Header.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
HStack,
55
Icon,
66
IconButton,
7-
Popover,
87
Text,
98
Tooltip,
109
} from "@chakra-ui/react";

src/components/projects/FormPayment.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ const FormPayment = ({
2929

3030
useQuery(
3131
"check-payment",
32-
() => axios.get(`/api/checkout/check/${query.ppi}/${query.session_id}`),
32+
() =>
33+
axios.get(`/api/checkout/check/${query.ppi}/${query.session_id}/studio`),
3334
{
3435
cacheTime: 0,
3536
refetchInterval: 10,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import React, { useEffect, useState } from "react";
2+
import {
3+
Menu,
4+
MenuButton,
5+
MenuList,
6+
MenuItem,
7+
Button,
8+
HStack,
9+
Text,
10+
} from "@chakra-ui/react";
11+
import { useRouter } from "next/router";
12+
import { useQuery } from "react-query";
13+
import axios from "axios";
14+
import { BsChevronDown } from "react-icons/bs";
15+
import { IoIosFlash } from "react-icons/io";
16+
17+
const BuyShotButton = ({
18+
credits,
19+
onPaymentSuccess,
20+
}: {
21+
credits: number;
22+
onPaymentSuccess: (credits: number) => void;
23+
}) => {
24+
const { push, query } = useRouter();
25+
const [waitingPayment, setWaitingPayment] = useState(false);
26+
27+
const { isLoading } = useQuery(
28+
"check-shot-payment",
29+
() =>
30+
axios.get(`/api/checkout/check/${query.ppi}/${query.session_id}/shot`),
31+
{
32+
cacheTime: 0,
33+
refetchInterval: 4,
34+
retry: 0,
35+
enabled: waitingPayment,
36+
onSuccess: (response) => {
37+
onPaymentSuccess(response.data.credits);
38+
},
39+
onSettled: () => {
40+
setWaitingPayment(false);
41+
},
42+
}
43+
);
44+
45+
useEffect(() => {
46+
setWaitingPayment(query.ppi === query.id);
47+
}, [query]);
48+
49+
const handleShotPayment = (quantity: number) => {
50+
push(`/api/checkout/shots?quantity=${quantity}&ppi=${query.id}`);
51+
};
52+
53+
return (
54+
<Menu>
55+
<MenuButton
56+
rightIcon={<BsChevronDown />}
57+
isLoading={isLoading}
58+
size="xs"
59+
shadow="none"
60+
variant="brand"
61+
as={Button}
62+
>
63+
<HStack spacing={0}>
64+
<IoIosFlash />
65+
{credits === 0 ? (
66+
<Text>Buy more shots</Text>
67+
) : (
68+
<Text>
69+
{credits} Shot{credits > 1 && "s"} left
70+
</Text>
71+
)}
72+
</HStack>
73+
</MenuButton>
74+
<MenuList fontSize="sm">
75+
<MenuItem
76+
command="$4"
77+
onClick={() => {
78+
handleShotPayment(100);
79+
}}
80+
>
81+
Add <b>100 shots</b>
82+
</MenuItem>
83+
<MenuItem
84+
command="$7"
85+
onClick={() => {
86+
handleShotPayment(200);
87+
}}
88+
>
89+
Add <b>200 shots</b>
90+
</MenuItem>
91+
<MenuItem
92+
command="$9"
93+
onClick={() => {
94+
handleShotPayment(300);
95+
}}
96+
>
97+
Add <b>300 shots</b>
98+
</MenuItem>
99+
</MenuList>
100+
</Menu>
101+
);
102+
};
103+
104+
export default BuyShotButton;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { NextApiRequest, NextApiResponse } from "next";
2+
import Stripe from "stripe";
3+
import db from "@/core/db";
4+
5+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
6+
apiVersion: "2022-11-15",
7+
});
8+
9+
export default async function handler(
10+
req: NextApiRequest,
11+
res: NextApiResponse
12+
) {
13+
const sessionId = req.query.sessionId as string;
14+
const ppi = req.query.ppi as string;
15+
16+
const session = await stripe.checkout.sessions.retrieve(sessionId);
17+
18+
const payments = await db.payment.findMany({
19+
where: {
20+
stripeSessionId: sessionId,
21+
projectId: ppi,
22+
status: "paid",
23+
type: "credits",
24+
},
25+
});
26+
27+
if (payments.length > 0) {
28+
return res
29+
.status(400)
30+
.json({ success: false, error: "payment_already_processed" });
31+
}
32+
33+
if (
34+
session.payment_status === "paid" &&
35+
session.metadata?.projectId === ppi
36+
) {
37+
const quantity = Number(session.metadata?.quantity);
38+
const project = await db.project.update({
39+
where: { id: ppi },
40+
data: { credits: { increment: quantity } },
41+
});
42+
43+
await db.payment.create({
44+
data: {
45+
status: "paid",
46+
projectId: ppi,
47+
type: "credits",
48+
stripeSessionId: sessionId,
49+
},
50+
});
51+
52+
return res.status(200).json({ success: true, credits: project.credits });
53+
}
54+
55+
return res.status(400).json({ success: false });
56+
}

src/pages/api/checkout/session.ts

-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,4 @@ export default async function handler(
3636
} catch (err: any) {
3737
return res.status(400).json(err.message);
3838
}
39-
40-
return res.status(400).json({});
4139
}

src/pages/api/checkout/shots.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { NextApiRequest, NextApiResponse } from "next";
2+
import Stripe from "stripe";
3+
4+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
5+
apiVersion: "2022-11-15",
6+
});
7+
8+
const PRICES = { 100: 400, 200: 700, 300: 900 };
9+
10+
export default async function handler(
11+
req: NextApiRequest,
12+
res: NextApiResponse
13+
) {
14+
const quantity = Number(req.query.quantity);
15+
const ppi = req.query.ppi;
16+
17+
if (quantity !== 100 && quantity !== 200 && quantity !== 300) {
18+
return res.status(400).json("invalid_quantity");
19+
}
20+
21+
try {
22+
const session = await stripe.checkout.sessions.create({
23+
allow_promotion_codes: true,
24+
metadata: {
25+
projectId: req.query.ppi as string,
26+
quantity,
27+
},
28+
line_items: [
29+
{
30+
price_data: {
31+
currency: "usd",
32+
unit_amount: PRICES[quantity],
33+
product_data: {
34+
name: `⚡️ Refill +${quantity} shots`,
35+
},
36+
},
37+
quantity: 1,
38+
},
39+
],
40+
mode: "payment",
41+
success_url: `${process.env.NEXTAUTH_URL}/studio/${ppi}/?session_id={CHECKOUT_SESSION_ID}&ppi=${ppi}`,
42+
cancel_url: `${process.env.NEXTAUTH_URL}/studio/${ppi}`,
43+
});
44+
45+
return res.redirect(303, session.url!);
46+
} catch (err: any) {
47+
return res.status(400).json(err.message);
48+
}
49+
}

src/pages/api/projects/[id]/predictions/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
2323
const { data } = await replicateClient.post(
2424
`https://api.replicate.com/v1/predictions`,
2525
{
26-
input: { prompt },
26+
input: {
27+
prompt,
28+
},
2729
version: project.modelVersionId,
2830
}
2931
);

0 commit comments

Comments
 (0)